docs(testing): more samples

This commit is contained in:
Ward Bell 2016-06-03 18:15:14 -07:00
parent 05864c2584
commit 7edc32f350
7 changed files with 265 additions and 116 deletions

View File

@ -13,6 +13,8 @@ import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component'; import { HeroDetailComponent } from './hero-detail.component';
import { HeroService } from './hero.service'; import { HeroService } from './hero.service';
import { BAG_DIRECTIVES, BAG_PROVIDERS } from './bag';
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
template: ` template: `
@ -22,12 +24,23 @@ import { HeroService } from './hero.service';
<a [routerLink]="['Heroes']">Heroes</a> <a [routerLink]="['Heroes']">Heroes</a>
</nav> </nav>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<hr>
<h1>Bag-a-specs</h1>
<my-if-parent-comp></my-if-parent-comp>
<h3>External Template Comp</h3>
<external-template-comp></external-template-comp>
<h3>Comp With External Template Comp</h3>
<comp-w-ext-comp></comp-w-ext-comp>
`, `,
/*
*/
styleUrls: ['app/app.component.css'], styleUrls: ['app/app.component.css'],
directives: [RouterLink, RouterOutlet], directives: [RouterLink, RouterOutlet, BAG_DIRECTIVES],
providers: [ providers: [
ROUTER_PROVIDERS, ROUTER_PROVIDERS,
HeroService HeroService,
BAG_PROVIDERS
] ]
}) })
@RouteConfig([ @RouteConfig([

View File

@ -4,7 +4,7 @@
* Tests that show what goes wrong when the tests are incorrectly written or have a problem * Tests that show what goes wrong when the tests are incorrectly written or have a problem
*/ */
import { import {
BadTemplateUrl, ButtonComp, BadTemplateUrlComp, ButtonComp,
ChildChildComp, ChildComp, ChildWithChildComp, ChildChildComp, ChildComp, ChildWithChildComp,
ExternalTemplateComp, ExternalTemplateComp,
FancyService, MockFancyService, FancyService, MockFancyService,
@ -134,7 +134,7 @@ xdescribe('async & inject testing errors', () => {
it('should fail with an error from a promise', it('should fail with an error from a promise',
async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
tcb.createAsync(BadTemplateUrl); tcb.createAsync(BadTemplateUrlComp);
}))); })));
itPromise.then( itPromise.then(

View File

@ -1,9 +1,9 @@
// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts // Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts
/* tslint:disable:no-unused-variable */ /* tslint:disable:no-unused-variable */
import { import {
BadTemplateUrl, ButtonComp, BadTemplateUrlComp, ButtonComp,
ChildChildComp, ChildComp, ChildWithChildComp, ChildChildComp, ChildComp, ChildWithChildComp,
ExternalTemplateComp, CompWithCompWithExternalTemplate, ExternalTemplateComp,
FancyService, MockFancyService, FancyService, MockFancyService,
InputComp, InputComp,
MyIfComp, MyIfChildComp, MyIfParentComp, MyIfComp, MyIfChildComp, MyIfParentComp,
@ -18,14 +18,16 @@ import { By } from '@angular/platform-browser';
import { import {
beforeEach, beforeEachProviders, beforeEach, beforeEachProviders,
describe, ddescribe, xdescribe, describe, ddescribe, xdescribe,
expect, it, iit, xit, it, iit, xit,
async, inject, async, inject,
fakeAsync, tick, withProviders fakeAsync, tick, withProviders
} from '@angular/core/testing'; } from '@angular/core/testing';
// https://github.com/angular/angular/issues/9017
import {expect} from './expect-proper';
import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing';
import { provide } from '@angular/core';
import { ViewMetadata } from '@angular/core'; import { ViewMetadata } from '@angular/core';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
@ -116,7 +118,7 @@ describe('using the test injector with the inject helper', () => {
describe('setting up Providers with FancyService', () => { describe('setting up Providers with FancyService', () => {
beforeEachProviders(() => [ beforeEachProviders(() => [
provide(FancyService, {useValue: new FancyService()}) {provide: FancyService, useValue: new FancyService()}
]); ]);
it('should use FancyService', it('should use FancyService',
@ -183,16 +185,42 @@ describe('using the test injector with the inject helper', () => {
describe('using `withProviders` for per-test provision', () => { describe('using `withProviders` for per-test provision', () => {
it('should inject test-local FancyService for this test', it('should inject test-local FancyService for this test',
// `withProviders`: set up providers at individual test level // `withProviders`: set up providers at individual test level
withProviders(() => [provide(FancyService, {useValue: {value: 'fake value'}})]) withProviders(() => [{provide: FancyService, useValue: {value: 'fake value'}}])
// now inject and test // now inject and test
.inject([FancyService], (service: FancyService) => { .inject([FancyService], (service: FancyService) => {
expect(service.value).toEqual('fake value'); expect(service.value).toEqual('fake value');
})); }));
}); });
describe('can spy on FancyService', () => {
let spy: jasmine.Spy;
let value: string;
beforeEachProviders(() => [
{provide: FancyService, useValue: new FancyService()}
]);
beforeEach(inject([FancyService], (service: FancyService) => {
spy = spyOn(service, 'getValue').and.callFake(() => 'fake value');
value = service.getValue();
}));
it('FancyService.getValue spy should return fake', () => {
expect(value).toBe('fake value');
});
it('FancyService.getValue spy should have been called', () => {
expect(spy.calls.count()).toBe(1, 'should be called once');
});
});
}); });
describe('test component builder', function() { describe('test component builder', function() {
beforeEachProviders(() => [ FancyService ]);
it('should instantiate a component with valid DOM', it('should instantiate a component with valid DOM',
async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
@ -314,7 +342,7 @@ describe('test component builder', function() {
tcb.overrideProviders( tcb.overrideProviders(
TestProvidersComp, TestProvidersComp,
[provide(FancyService, {useClass: MockFancyService})] [{provide: FancyService, useClass: MockFancyService}]
) )
.createAsync(TestProvidersComp) .createAsync(TestProvidersComp)
.then(fixture => { .then(fixture => {
@ -329,7 +357,7 @@ describe('test component builder', function() {
tcb.overrideViewProviders( tcb.overrideViewProviders(
TestViewProvidersComp, TestViewProvidersComp,
[provide(FancyService, {useClass: MockFancyService})] [{provide: FancyService, useClass: MockFancyService}]
) )
.createAsync(TestViewProvidersComp) .createAsync(TestViewProvidersComp)
.then(fixture => { .then(fixture => {
@ -339,7 +367,7 @@ describe('test component builder', function() {
}); });
}))); })));
it('should allow an external templateUrl', it('should allow an external template',
async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
tcb.createAsync(ExternalTemplateComp) tcb.createAsync(ExternalTemplateComp)
@ -348,130 +376,201 @@ describe('test component builder', function() {
expect(fixture.nativeElement) expect(fixture.nativeElement)
.toHaveText('from external template\n'); .toHaveText('from external template\n');
}); });
})), 10000); // Long timeout because this test makes an actual XHR. })), 10000); // Long timeout because of actual XHR to fetch template.
describe('(lifecycle hooks w/ MyIfParentComp)', () => { it('should create a component with a component that has an external template',
let fixture: ComponentFixture<MyIfParentComp>; async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
let parent: MyIfParentComp; tcb.createAsync(CompWithCompWithExternalTemplate)
let child: MyIfChildComp; .then(fixture => {
fixture.detectChanges();
let h3 = fixture.debugElement.query(By.css('h3'));
expect(h3).not.toBeNull('should create CompWithExtComp component');
})
.catch((err) => {
// console.error(err);
throw (err);
});
})), 10000); // Long timeout because of actual XHR to fetch template.
/**
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
*/
function getChild() {
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp it('should spy on injected component service',
async(inject([TestComponentBuilder, FancyService],
(tcb: TestComponentBuilder, service: FancyService) => {
// The Hard Way: requires detailed knowledge of the parent template let spy = spyOn(service, 'getValue').and.callThrough();
try {
childDe = fixture.debugElement.children[4].children[0];
} catch (err) { /* we'll report the error */ }
// DebugElement.queryAll: if we wanted all of many instances: tcb
childDe = fixture.debugElement .createAsync(ExternalTemplateComp)
.queryAll(function (de) { return de.componentInstance instanceof MyIfChildComp; })[0]; .then(fixture => {
// WE'LL USE THIS APPROACH ! // let spy = spyOn(service, 'getValue').and.callThrough();
// DebugElement.query: find first instance (if any)
childDe = fixture.debugElement
.query(function (de) { return de.componentInstance instanceof MyIfChildComp; });
if (childDe && childDe.componentInstance) { fixture.detectChanges();
child = childDe.componentInstance; expect(spy.calls.count()).toBe(1, 'should be called once');
} else { });
fail('Unable to find MyIfChildComp within MyIfParentComp'); })), 10000); // Long timeout because of actual XHR to fetch template.
}
return child; describe('(lifecycle hooks w/ MyIfParentComp)', () => {
let fixture: ComponentFixture<MyIfParentComp>;
let parent: MyIfParentComp;
let child: MyIfChildComp;
/**
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
*/
function getChild() {
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp
// The Hard Way: requires detailed knowledge of the parent template
try {
childDe = fixture.debugElement.children[4].children[0];
} catch (err) { /* we'll report the error */ }
// DebugElement.queryAll: if we wanted all of many instances:
childDe = fixture.debugElement
.queryAll(function (de) { return de.componentInstance instanceof MyIfChildComp; })[0];
// WE'LL USE THIS APPROACH !
// DebugElement.query: find first instance (if any)
childDe = fixture.debugElement
.query(function (de) { return de.componentInstance instanceof MyIfChildComp; });
if (childDe && childDe.componentInstance) {
child = childDe.componentInstance;
} else {
fail('Unable to find MyIfChildComp within MyIfParentComp');
} }
// Create MyIfParentComp TCB and component instance before each test (async beforeEach) return child;
beforeEach(async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { }
tcb.createAsync(MyIfParentComp)
.then(fix => {
fixture = fix;
parent = fixture.debugElement.componentInstance;
});
})));
it('should instantiate parent component', () => { // Create MyIfParentComp TCB and component instance before each test (async beforeEach)
expect(parent).not.toBeNull('parent component should exist'); beforeEach(async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
}); tcb.createAsync(MyIfParentComp)
.then(fix => {
fixture = fix;
parent = fixture.debugElement.componentInstance;
});
})));
it('parent component OnInit should NOT be called before first detectChanges()', () => { it('should instantiate parent component', () => {
expect(parent.ngOnInitCalled).toEqual(false); expect(parent).not.toBeNull('parent component should exist');
}); });
it('parent component OnInit should be called after first detectChanges()', () => { it('parent component OnInit should NOT be called before first detectChanges()', () => {
fixture.detectChanges(); expect(parent.ngOnInitCalled).toEqual(false);
expect(parent.ngOnInitCalled).toEqual(true); });
});
it('child component should exist after OnInit', () => { it('parent component OnInit should be called after first detectChanges()', () => {
fixture.detectChanges(); fixture.detectChanges();
getChild(); expect(parent.ngOnInitCalled).toEqual(true);
expect(child instanceof MyIfChildComp).toEqual(true, 'should create child'); });
});
it('should have called child component\'s OnInit ', () => { it('child component should exist after OnInit', () => {
fixture.detectChanges(); fixture.detectChanges();
getChild(); getChild();
expect(child.ngOnInitCalled).toEqual(true); expect(child instanceof MyIfChildComp).toEqual(true, 'should create child');
}); });
it('child component called OnChanges once', () => { it('should have called child component\'s OnInit ', () => {
fixture.detectChanges(); fixture.detectChanges();
getChild(); getChild();
expect(child.ngOnChangesCounter).toEqual(1); expect(child.ngOnInitCalled).toEqual(true);
}); });
it('changed parent value flows to child', () => { it('child component called OnChanges once', () => {
fixture.detectChanges(); fixture.detectChanges();
getChild(); getChild();
expect(child.ngOnChangesCounter).toEqual(1);
});
parent.parentValue = 'foo'; it('changed parent value flows to child', () => {
fixture.detectChanges();
getChild();
parent.parentValue = 'foo';
fixture.detectChanges();
expect(child.ngOnChangesCounter).toEqual(2,
'expected 2 changes: initial value and changed value');
expect(child.childValue).toEqual('foo',
'childValue should eq changed parent value');
});
it('changed child value flows to parent', async(() => {
fixture.detectChanges();
getChild();
child.childValue = 'bar';
return new Promise(resolve => {
// Wait one JS engine turn!
setTimeout(() => resolve(), 0);
}).then(() => {
fixture.detectChanges(); fixture.detectChanges();
expect(child.ngOnChangesCounter).toEqual(2, expect(child.ngOnChangesCounter).toEqual(2,
'expected 2 changes: initial value and changed value'); 'expected 2 changes: initial value and changed value');
expect(child.childValue).toEqual('foo', expect(parent.parentValue).toEqual('bar',
'childValue should eq changed parent value'); 'parentValue should eq changed parent value');
}); });
it('changed child value flows to parent', async(() => { }));
fixture.detectChanges();
getChild();
child.childValue = 'bar'; it('clicking "Close Child" triggers child OnDestroy', () => {
fixture.detectChanges();
getChild();
return new Promise(resolve => { let btn = fixture.debugElement.query(By.css('button'));
// Wait one JS engine turn! btn.triggerEventHandler('click', null);
setTimeout(() => resolve(), 0);
}).then(() => {
fixture.detectChanges();
expect(child.ngOnChangesCounter).toEqual(2,
'expected 2 changes: initial value and changed value');
expect(parent.parentValue).toEqual('bar',
'parentValue should eq changed parent value');
});
}));
it('clicking "Close Child" triggers child OnDestroy', () => {
fixture.detectChanges();
getChild();
let btn = fixture.debugElement.query(By.css('button'));
btn.triggerEventHandler('click', null);
fixture.detectChanges();
expect(child.ngOnDestroyCalled).toEqual(true);
});
fixture.detectChanges();
expect(child.ngOnDestroyCalled).toEqual(true);
}); });
});
}); });
describe('test component builder in beforeEach (comp w/ external template)', function() {
let fixture: ComponentFixture<ExternalTemplateComp>;
beforeEach(async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
tcb.createAsync(ExternalTemplateComp).then(fix => fixture = fix);
}))
);
// May need Long timeout because this test makes an actual XHR to get template
// , 10000); WHY CAN'T I ADD TIMEOUT OVERRIDE
it('should allow an external template', () => {
fixture.detectChanges();
expect(fixture.nativeElement)
.toHaveText('from external template\n');
});
});
describe('test component builder in beforeEach (comp w/ internal comp w/ external template)', function() {
let fixture: ComponentFixture<CompWithCompWithExternalTemplate>;
beforeEach(async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
tcb.createAsync(CompWithCompWithExternalTemplate).then(fix => fixture = fix)
.catch((err) => {
// console.error(err);
throw (err);
});
}))
);
// May need Long timeout because this test makes an actual XHR to get template
// , 10000); WHY CAN'T I ADD TIMEOUT OVERRIDE
it('should allow an external template', () => {
fixture.detectChanges();
let h3 = fixture.debugElement.query(By.css('h3'));
expect(h3).not.toBeNull('should create CompWithExtComp component');
});
});
//////// Testing Framework Bugs? ///// //////// Testing Framework Bugs? /////
import { HeroService } from './hero.service'; import { HeroService } from './hero.service';
@ -495,7 +594,7 @@ describe('tcb.overrideProviders', () => {
tcb.overrideProviders( tcb.overrideProviders(
AnotherProvidersComp, AnotherProvidersComp,
[provide(HeroService, {useValue: {}})] [{provide: HeroService, useValue: {}}]
) )
.createAsync(AnotherProvidersComp); .createAsync(AnotherProvidersComp);
}))); })));

View File

@ -1,6 +1,6 @@
// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts // Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts
/* tslint:disable:forin */ /* tslint:disable:forin */
import { Component, EventEmitter, Injectable, Input, Output, import { Component, EventEmitter, Injectable, Input, Output, Optional,
OnInit, OnChanges, OnDestroy, SimpleChange } from '@angular/core'; OnInit, OnChanges, OnDestroy, SimpleChange } from '@angular/core';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
@ -13,6 +13,8 @@ import { Observable } from 'rxjs/Rx';
export class FancyService { export class FancyService {
value: string = 'real value'; value: string = 'real value';
getValue() { return this.value; }
getAsyncValue() { return Promise.resolve('async value'); } getAsyncValue() { return Promise.resolve('async value'); }
getObservableValue() { return Observable.of('observable value'); } getObservableValue() { return Observable.of('observable value'); }
@ -123,20 +125,36 @@ export class TestViewProvidersComp {
constructor(private fancyService: FancyService) {} constructor(private fancyService: FancyService) {}
} }
@Component({ @Component({
moduleId: module.id, moduleId: module.id,
selector: 'external-template-comp', selector: 'external-template-comp',
templateUrl: 'bag-external-template.html' templateUrl: 'bag-external-template.html'
}) })
export class ExternalTemplateComp { } export class ExternalTemplateComp {
serviceValue: string;
constructor(@Optional() private service: FancyService) { }
ngOnInit() {
if (this.service) { this.serviceValue = this.service.getValue(); }
}
}
@Component({
selector: 'comp-w-ext-comp',
template: `
<h3>comp-w-ext-comp</h3>
<external-template-comp></external-template-comp>
`,
directives: [ExternalTemplateComp]
})
export class CompWithCompWithExternalTemplate { }
@Component({ @Component({
selector: 'bad-template-comp', selector: 'bad-template-comp',
templateUrl: 'non-existant.html' templateUrl: 'non-existant.html'
}) })
export class BadTemplateUrl { } export class BadTemplateUrlComp { }
///////// MyIfChildComp //////// ///////// MyIfChildComp ////////
@ -222,3 +240,16 @@ export class MyIfParentComp implements OnInit {
this.toggleLabel = this.showChild ? 'Close' : 'Show'; this.toggleLabel = this.showChild ? 'Close' : 'Show';
} }
} }
export const BAG_PROVIDERS = [FancyService];
export const BAG_DIRECTIVES = [
ButtonComp,
ChildChildComp, ChildComp, ChildWithChildComp,
ExternalTemplateComp, CompWithCompWithExternalTemplate,
InputComp,
MyIfComp, MyIfChildComp, MyIfParentComp,
MockChildComp, MockChildChildComp,
ParentComp,
TestProvidersComp, TestViewProvidersComp
];

View File

@ -0,0 +1,9 @@
// See https://github.com/angular/angular/issues/9017
import { expect as expectCore} from '@angular/core/testing';
import { NgMatchers } from '@angular/platform-browser/testing';
export function expect(spy: Function): NgMatchers;
export function expect(actual: any): NgMatchers;
export function expect(actual: any): NgMatchers {
return expectCore(actual) as NgMatchers;
}

View File

@ -1,6 +1,5 @@
import { bootstrap } from '@angular/platform-browser-dynamic'; import { bootstrap } from '@angular/platform-browser-dynamic';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { MyIfParentComp } from './bag';
bootstrap(AppComponent); bootstrap(AppComponent);
bootstrap(MyIfParentComp);

View File

@ -22,7 +22,5 @@
<body> <body>
<my-app>Loading...</my-app> <my-app>Loading...</my-app>
<hr>
<my-if-parent-comp>Loading MyIfParentComp ...</my-if-parent-comp>
</body> </body>
</html> </html>