Table of Contents generated with DocToc
- Unit testing examples:
- Integration testing examples
import { VoteComponent } from './vote.component';
describe('VoteComponent', () => {
let component: VoteComponent;
// Arrage
// each test should run in an isolated world
beforeAll(() => {
// what needs to be executed before all tests
});
beforeEach(() => {
// setup
// before each test, component should be reinitialized so that previously runned test won't affect running test
component = new VoteComponent();
})
afterEach(() => {
// tear down
});
// define as many tests as there are execution paths for all methods of the component
// a test or spec is defined by it() function
it('should increment totalVotes when upvoted', () => {
// Act - call a method
component.upVote();
// Assert
expect(component.totalVotes).toBe(1);
});
it('should decrement totalVotes when downvoted', () => {
// Act - call a method
component.downVote();
// Assert
expect(component.totalVotes).toBe(-1);
});
afterAll(() => {
// what needs to be executed after all tests
})
});
export class TodoFormComponent {
form: FormGroup;
constructor(fb: FormBuilder) {
this.form = fb.group({
name: ['', Validators.required],
email: [''],
});
}
}
we have the following unit tests:
describe('TodoFormComponent', () => {
var component: TodoFormComponent;
beforeEach(() => {
component = new TodoFormComponent(new FormBuilder());
});
it('should create a form with 2 controls', () => {
expect(component.form.contains('name')).toBeTruthy();
expect(component.form.contains('email')).toBeTruthy();
});
it('should make the name control required', () => {
let control = component.form.get('name');
control.setValue('');
expect(control.valid).toBeFalsy();
});
});
import { EventEmitter } from '@angular/core';
export class VoteComponent {
totalVotes = 0;
voteChanged = new EventEmitter();
upVote() {
this.totalVotes++;
this.voteChanged.emit(this.totalVotes);
}
}
unit test:
it('should raise voteChanged event when upvoted', () => {
// beause event emitter is an observabale we can subscribe to get the event raised
// INITIALIZATION
let totalVotes = null;
component.voteChanged.subscribe(tv => totalVotes = tv);
// ACT
component.upVote();
// expect(component.totalVotes).not.toBeNull(); this test will pass even though we have a bug in the component
expect(component.totalVotes).toBe(1);
});
Component:
import { TodoService } from './todo.service'
export class TodosComponent {
todos: any[] = [];
message;
constructor(private service: TodoService) {}
ngOnInit() {
this.service.getTodos().subscribe(t => this.todos = t);
}
add() {
var newTodo = { title: '... ' };
this.service.add(newTodo).subscribe(
t => this.todos.push(t),
err => this.message = err);
}
delete(id) {
if (confirm('Are you sure?'))
this.service.delete(id).subscribe();
}
}
Testing:
describe('TodosComponent', () => {
let component: TodosComponent;
let service: TodoService;
beforeEach(() => {
service = new TodoService(null); // We cheat - anyway we will not use the Http protocol
component = new TodosComponent(service);
});
xit('should set todos property with the items returned from server', () => {
let todos = [1, 2, 3];
// Arrange:
// we want to change the implementation of the getTodos method by using the spyOn method from Jasmine
// with spyOn we get control over a method in a class - can check if a method has been called, we can change it's implementation or return a different value
spyOn(service, 'getTodos').and.returnValue(from([[1, 2, 3]]));
// Action
component.ngOnInit();
// Assert
expect(component.todos.length).toBe(3);
expect(component.todos).toBe(todos);
});
it('should call the server to save the chages when a new todo item is added'), () => {
// arrange - make sure that the add method from service is called
let spy = spyOn(service, 'add').and.callFake(todoItem => {
return empty();//from([[1, 2, 3, 4]]);
});
component.add();
expect(spy).toHaveBeenCalled();
}
it('should add a new todo from the server'), () => {
let todo = { id: 1 };
// arrange - make sure that the add method from service is called
let spy = spyOn(service, 'add').and.returnValue(from([{ id: 1 }]));
component.add();
expect(component.todos.indexOf(todo)).toBeGreaterThan(-1);
}
it('should set the message property when server gets an error when adding a new todo'), () => {
let error = 'error from the server';
// arrange - make sure that the add method from service is called
let spy = spyOn(service, 'add').and.returnValue(Observable.throw(error));
component.add();
expect(component.message).toBe(error);
}
it('should call the server to delete a todo item if the user confirms'), () => {
let todoId = 1;
// arrange - make sure that the add method from service is called
spyOn(window, 'confirm').and.returnValue(true);
let spy = spyOn(service, 'delete').and.returnValue(empty());
// act
component.delete(todoId);
expect(spy).toHaveBeenCalledWith(todoId);
}
it('should NOT call the server to delete a todo item if the user cancels'), () => {
let todoId = 1;
// arrange - make sure that the add method from service is called
spyOn(window, 'confirm').and.returnValue(false);
let spy = spyOn(service, 'delete').and.returnValue(empty());
// act
component.delete(todoId);
expect(spy).not.toHaveBeenCalled();
}
});
Angular's CLI will generate the spec files along with componnet/service/pipe/directive genaration;
For unit testing can rename the .spec file to unit.spec and have separated spec file for integration testing.
A generated .spec file contains:
// we run an asynchronous function that will return a normal function to execute before each test execution
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AuthorsComponent ]
})
.compileComponents(); // the async function execution is done because it takes time to compile all components from
// declarations array along with their templates and syles
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuthorsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
Because the standard compile way in Angular is done using Webpack, all components,templates and styles are bundled into one single .js file, thus the compileComponets() is not necessary and we can have just one single beforeEach block.
describe('VoterComponent', () => {
let component: VoterComponent;
let fixture: ComponentFixture<VoterComponent>;
beforeEach(() => {
// for integration testing we cannot create the instance of the component
// we need to ask Angular to create that instance for us
// using utility methods from TestBed class we, first create a dynamic module for our component
TestBed.configureTestingModule({
declarations: [VoterComponent] // for this simple component there's no need of imports or providers array
});
// now we instruct Angular to create the component
// TestBed.createComponent(VoterComponent); // this returns a component fixture of type <VoterComponent>
// so this component fixture is a wrapper over our component's instance and also over its template
fixture = TestBed.createComponent(VoterComponent);
// use it to get an instange of the component, can run change detection manually and also get one or more of the injected dependencies in this component,
component = fixture.componentInstance;
// the fixture.nativeElement = the root DOM element for this component's template
// the fixture.debugElement = a wrapper around nativeElement providing useful methods for quering the DOM
});
});
We are interested to check what exactly we have in the DOM (HTML template), thus we use the query method of debuggerElement wrapper to traverse the DOM and find elements corresponding either to css selectors or HTML attributes
For voter componet we have the following template:
<i
class="glyphicon glyphicon-menu-up vote-button"
[class.highlighted]="myVote == 1"
(click)="upVote()"></i>
<span class="vote-count">{{ totalVotes }}</span>
a) Check if the innerText of the element with .vote-count
class contains the proper totalVotes string:
it('should render total votes', () => {
component.myVote = 1;
component.othersVote = 20;
// need to explicitlly tell Angular when to perform change detection
// because in testing env, it does not run as it normally does
fixture.detectChanges();
let debugElem = fixture.debugElement.query(By.css('vote-count'));
// returns first HTML element that matches the interogation
// can use By with .directive instead of .css and it will return the
// DOM element on which the specified directive is applied
let el = debugElem.nativeElement; // debugElement is of type HTMLElement
expect(el.innerText).toContain(21);// if we would have used toBe the test would have been too fragile
// when working with strings better use toContain instead of toBe
});
b) Check if, when I have upvoted, then the icon with class .glyphicon-menu-up
should also have the highlighted
class;
it('should highlight the upvoted button, if I had upvoted', () => {
component.myVote = 1;
fixture.detectChanges();
let debugElem = fixture.debugElement.query(By.css('.glyphicon-menu-up'));
// no need for native element to check for the existence of a class on the
// html element - use classes property of debugElem (can acess also styles and attributes properties)
expect(debugElem.classes['highlighted']).toBeTruthy();
});
it('should increase total votes when I click the upvote button', () => {
let button = fixture.debugElement.query(By.css('.glyphicon-menu-up'));
// we need to explicitly trigger the click:
button.triggerEventHandler('click', null); // this is for integration testing
// for Unit testing you will simply have: component.upVote();
expect(component.totalVotes).toBe(1);
});
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TodosComponent],
providers: [TodoService],
imports:[HttpClient]
})
.compileComponents();
}));
If the service is registered in the providers array,at the level of the module, this will become a singleton = a single instace shared accross all components in module then you can get this service dependency from the TestBed:
let service = TestBed.get(TodoService);
If the service is registered directly in the component's metadata, then you will get the corresponding service dependecy, injected in the component, from the debugElement wrapper:
let service = fixture.debugElement.injector.get(TodoService);
Delete the fixture.detectChanges();
generated along with component because calling this method before each test execution will make Angular to execute ngOnInit with all it's API calls/methods before getting the chance to spyOn on these methods called in ngOnInit, in order to change the behavior and return testing values.
Thus:
it('should load todos from the server', () => {
let service = TestBed.get(TodoService);
spyOn(service, 'getTodos').and.returnValue(from([1, 2, 3]));
fixture.detectChanges(); // trigger Angular's detect change mechanism only after apply spyOn to the methods from ngOnInit
expect(component.todos.length).toBe(3);
});
Inside user-details.spec.ts we want to ensure that the navigate method of the router is called with the right arguments. For:
save() {
this.router.navigate(['users']);
}
Testing using the router itself is part of Angular's testing, not our app testing. Thus, we provide a stub = a fake router which will be a lightweight implementation of Angular's router that has onlu methods that we have used inside our component. we need:
// ...
class RouterStub {
navigate(params) {
}
}
// ...
TestBed.configureTestingModule({
declarations: [UserDetailsComponent],
// we register RouterStub class as a provider, insted on Router itself
providers: [
// we tell Angular that when it sees injected into component's constructor an instance of Router class
// it should create an instance of RouterStub, and use this object instead of Router's instance
{ provide: Router, useClass: RouterStub}
]
Similarly, use stubs for all the other dependencies that have been injected into the component.
it('should redirect the user to the user\'s page after saving', () => {
let router = TestBed.get(Router);
let spy = spyOn(router, 'navigate'); // we already use a stub and there is no need to .and.callFake...
component.save();
expect(spy).toHaveBeenCalledWith(['users']);
});
Second test of route did not work ???. Also running:
npm test src/app/app-routing.spec.ts
test failed :-??
import { UsersComponent } from './components/user-component/users.component';
import { learnUnitTestRoutes } from './learn-unit-testing.module';
describe('learn-unit-testing module\'s routes', () => {
xit('should contain a route for /users', () =>
{
// it does not pass - ???
let childrenRoutes: Route[] = learnUnitTestRoutes[0].children;
expect(childrenRoutes).toContain({ path: 'users', component: UsersComponent });
// I do not know how to fix it
})
})
Because inside component's implementation we are using the params property from ActivatedRoute:
ngOnInit() {
this.route.params.subscribe(p => {
if (p['id'] === 0)
this.router.navigate(['not-found']);
});
}
we need to get and test params passed in the route:
it('should redirect the user to the not found page if an invalid user id is passed', () => {
let router = TestBed.get(Router);
// put a spy on the route to assert the navigate method has been called
let spy = spyOn(router, 'navigate');
let route: ActivatedRouteStub = TestBed.get(ActivatedRoute);
// we need to push a value in the observable stream of route's params
// on route.params we have only methods to read, not to push any data,
// but we'll use the ActivatedRouteStub, to have fake needed route's params
route.push({ id: 0 });
expect(spy).toHaveBeenCalledWith(['not-found']);
});
where:
class ActivatedRouteStub {
private subject = new Subject();
// Subject it is an Observable, but it has also Observer capabilities and
// we can use it to push values into the observable
push(value) {
this.subject.next(value);
}
// declare params as a public property:
get params() {
return this.subject.asObservable();
}
}
For me the test did not pass - no method call ?? anyway, the redirect is done inside ngOnInit, thus before:
fixture.componentInstance
- called before each test.
We need to ensure that:
<router-outlet></router-outlet>
it is present inside app.component.ts, thus inside the app.component.spec.ts:
it('should have a router outlet', () => {
const fixture = TestBed.createComponent(AppComponent);
const de = fixture.debugElement.query(By.directive(RouterOutlet))
expect(de).not.toBeNull;
});
for this to work we need to use Angular's RouterTestingModule class like this:
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([]),
// ...
],
//...
For testing if specific router links are present we need to check if specific href value exists among all tags with routerLink attribute, e.g. for:
<a class="nav-link" routerLink="about">About Info</a>
Will query component's wrapper by the RouterLinkWithHref
directive:
it('should have a link to about page', () => {
const fixture = TestBed.createComponent(AppComponent);
const debugElements = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref))
let index = debugElements.findIndex(de => de.attributes['routerLink'] === 'about');
expect(index).toBeGreaterThan(-1);
});
For complex templates, with many child templates(components), for testing you will either need to declare each child component inside the testing module, or, the simples way is to add: schemas: [NO_ERRORS_SCHEMA]
, after including just the main component in declarations array:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
AboutComponent,
LoginComponent,
SignUpComponent,
AlertDirective,
RepeaterDirective,
],
// schemas: [NO_ERRORS_SCHEMA], use this when you have too many components in the declarations array
// thought it is safer for testing to include all your components
// for me, using schemas, did not work, I had to include components
For testing attribete directives, the key is to use a DirectiveHostComponent and configure testing modules as below:
@Component({
template: `
<p highlight="cyan">First</p>
<p highlight>Second</p>
`
})
class DirectiveHostComponent {
}
// ...
describe('HighlightDirective', () => {
let fixture: ComponentFixture<DirectiveHostComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DirectiveHostComponent, HighlightDirective ]
})
.compileComponents();
}));
//...
it('should highlight the first element with cyan', () => {
let de = fixture.debugElement.queryAll(By.css('p'))[0];
expect(de.nativeElement.style.backgroundColor).toBe('cyan');
});
it('should highlight the second element with default color', () => {
let de = fixture.debugElement.queryAll(By.css('p'))[1];
// get the reference to the highlight directive in order to read it's default color
let directive = de.injector.get(HighlightDirective);
expect(de.nativeElement.style.backgroundColor).toBe(directive.defaultColor);
});
it('should load todos from the server via Promise', async(() => {
let service = TestBed.get(TodoService);
spyOn(service, 'getTodosPromise').and.returnValue(Promise.resolve([1, 2, 3]));
fixture.detectChanges(); // at this step ngOnInit is triggered
// but the async operation to fetch data are not yet completed when below is executed
// thus, we have to use the whenStable method which resolves a promise when all async operations from our component
// are already completed
fixture.whenStable().then(() => {
expect(component.todos.length).toBe(3);
});
console.log('EXPECTED WAS CALLED'); // because outside whenStable it gets execued before promise's resolve from ngOnInit
}));
or:
it('should load todos from the server via Promise fakeAsync', fakeAsync(() => {
let service = TestBed.get(TodoService);
spyOn(service, 'getTodosPromise').and.returnValue(Promise.resolve([1, 2, 3]));
fixture.detectChanges();
tick(); // this simulates the passage of time
expect(component.todos.length).toBe(3);
// because this is after tick, will exe after all async op from component are finished
console.log('EXPECTED WAS CALLED');
}));
export class QuestionsIndexComponent
{
n: number = 0;
// if you do not need your data update in the view constantly, for operations that happen inside a component
// then, you would use ngZone - here you will get updated data of component only when click event triggers method1 of component
constructor(@Inject(NgZone) private zone: NgZone)
{
this.zone.runOutsideAngular( () => {
// run this code in the background, outside Angular Zone - thus outside component's zone => component view
// does not get updated via data binding (e.g {{n}} in the html template)
setInterval( () => {
this.n = this.n + 1;
console.log(this.n);
}, 300);
} );
}
method1()
{
}
<span>{{n}}</span>
<input type="button" value="Click me" (click)="method1()">