CRM App - Part 14 Testing
Overview
Time: 5min
In this module, we will learn to use the Jasmine Testing Framework Basics
Learning Outcomes:
- How to write BDD (Behavior Driven Design) unit tests with Jasmine
Jasmine Introduction
Time: 5min
Jasmine is an open source testing framework from Pivotal Labs that uses behaviour-driven notation resulting in a fluent and improved testing experience.
Main concepts:
- Suites — describe(string, function) functions, take a title and a function containing one or more specs.
- Specs — it(string, function) functions, take a title and a function containing one or more expectations.
- Expectations — are assertions that evaluate to true or false. Basic syntax reads expect(actual).toBe(expected)
- Matchers — are predefined helpers for common assertions. Eg: toBe(expected), toEqual(expected). Find a complete list here.
Delete the contents of the auto generated AppComponent Spec file
Time: 5min
- Delete the default tests made by the Angular CLI
**src/app/app.component.spec.ts
Add a simple test (1+1=2)
Time: 10min
- Add a
describe
block.
src/app/app.component.spec.ts
describe(`Component: App Component`, () => {
});
src/app/app.component.spec.ts
- Add a
it
block.
describe(`Component: App Component`, () => {
it('add 1+1 - PASS', () => {
});
});
src/app/app.component.spec.ts
- Add an expectation.
describe(`Component: App Component`, () => {
it('add 1+1 - PASS', () => {
expect(1 + 1).toEqual(2);
});
});
Run the test with Karma test runner
- In the terminal at the path of the project start the unit tests with the following command
ng test
Figure: Karma output in terminal
Add a simple failing test (1+1=42)
Time: 1min
- Add another test, this time with a failing assumption.
src/app/app.component.spec.ts
describe(`Component: App Component`, () => {
it('add 1+1 - PASS', () => {
expect(1 + 1).toEqual(2);
});
it('add 1+1 - FAIL', () => {
expect(1 + 1).toEqual(42);
});
});
- run ng test again. This time, you should see one failure.
Add another test to check a property
Time: 10min
-
remove the failing test
-
Under the last
it
block add anotherit
block and write a test to check the title property is set correctly
src/app/app.component.spec.ts
import { AppComponent } from './app.component';
describe(`Component: App Component`, () => {
it('add 1+1 - PASS', () => {
expect(1 + 1).toEqual(2);
});
it(`title equals 'Angular Superpowers'`, () => {
const component = new AppComponent(null as any as CompanyService);
expect(component.title).toEqual('Angular Superpowers');
});
});
- set the title accordingly in the component
src/app/app.component.ts
export class AppComponent implements OnInit {
title = 'Angular Superpowers';
...
}
-
Note that the AppComponent requires a CompanyService parameter. For now, we won't use it in the test though, so we just pass null.
-
Karma should still be running from your last test and on save of the spec file automatically re-run.
Test the Component logic using a mocked up Service
Time: 15min
- Add a 'BeforeEach' section before the tests. This will run before each test ('it' sections).
src/app/app.component.spec.ts
...
describe('AppComponent', () => {
let component: AppComponent;
let companySvc: CompanyService;
beforeEach(() => {
companySvc = {
getCompanies: () => of([{
name: 'Fake Company',
email : 'fakeEmail@ssw.com.au',
phone: 12345,
}])
} as CompanyService;
component = new AppComponent(companySvc);
});
...
- Add another test for the companyCount$ property
src/app/app.component.spec.ts
...
it(`companyCount = 1`, () => {
component.ngOnInit();
component.companyCount$.subscribe(c => {
expect(c).toEqual(1);
});
});
...
Test the Component logic using SpyOn
Time: 10min
SpyOn is a Jasmin feature that allows dynamically intercepting the calls to a function and change its result. This example shows how spyOn works, even if we are still mocking up our service.
- Change the Mockup service so getCompanies returns nothing
src/app/app.component.spec.ts
...
beforeEach(() => {
companySvc = {
getCompanies: () => {}
} as CompanyService;
component = new AppComponent(companySvc);
});
...
- Add a new Test case
src/app/app.component.spec.ts
...
it(`companyCount = 2`, () => {
spyOn(companySvc, 'getCompanies').and.returnValue(of([
{
name: "Fake Company A",
email: "fakeEmail@ssw.com.au",
phone: 12345
} as Company,
{
name: "Fake Company B",
email: "fakeEmail@ssw.com.au",
phone: 12345
} as Company
]))
component.ngOnInit();
component.companyCount$.subscribe(c => {
expect(c).toEqual(2);
});
});
...
TestBed and Fixtures
Time: 10min
The TestBed is the first and largest of the Angular testing utilities. It creates an Angular testing module — a @NgModule class — that you configure with the configureTestingModule method to produce the module environment for the class you want to test.
- Remove the newed up AppComponent and replace it with a TestBed module
src/app/app.component.spec.ts
...
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { of } from 'rxjs';
...
let fixture: ComponentFixture<AppComponent>;
let component: AppComponent;
let companySvc: CompanyService;
let de: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
CompanyListComponent, // Our routing module needs it
CompanyTableComponent, // Our routing module needs it
CompanyEditComponent, // Our routing module needs it
],
imports: [
AppRoutingModule, // Routerlink in AppComponent needs it
HttpClientModule,
ReactiveFormsModule
],
providers: [{provide: APP_BASE_HREF, useValue: '/'}]
});
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
companySvc = TestBed.inject(CompanyService);
});
...
IMPORTANT - You need to copy the whole module declaration from 'src/app/app.module.ts' as we are mocking up the complete Angular Module.
You will also need to copy all imports from the very top of you module file.
We now have a completely mocked up module, with our AppComponent and CompanyService bound to it.
- Change the test to use the created fixture
src/app/app.component.spec.ts
...
it(`companyCount = 1`, () => {
spyOn(companySvc, 'getCompanies').and.returnValue(
of([
{
name: 'Fake Company C',
email: 'fakeEmail@ssw.com.au',
phone: 12345,
} as Company,
])
);
fixture.detectChanges();
expect(
component.companyCount$.subscribe((c) => {
expect(c).toEqual(1);
})
);
});
...
Now we have a test that runs against a completely mocked up AppComponent bundled with its dependencies. The fixture creates the component just like it the browser would in real life. Using DebugElement, you can even query the DOM for further tests.
- Make sure the navigation DOM has an idea on the span containing companyCount, as we will use a CSS Selector to query the DOM
src/app/app.component.html
...
<li class="nav-item" [routerLinkActive]="'active'">
<a class="nav-link" [routerLink]="['/company/list']">
Companies (<span id="company-count">{{ companyCount$ | async }}</span>)
</a>
</li>
...
- Create a new test to query the DOM
src/app/app.component.spec.ts
...
import { By } from '@angular/platform-browser';
...
it(`CompanyCount HTML should update`, () => {
spyOn(companySvc, 'getCompanies').and.returnValue(
of([
{
name: 'Fake Company C',
email: 'fakeEmail@ssw.com.au',
phone: 12345,
} as Company,
])
);
fixture.detectChanges();
let el = fixture.debugElement.query(By.css('#company-count')).nativeElement;
expect(el.textContent).toEqual('1');
});
...