Angular-Workshop: Unit-Testing
Preparation
If you have .spec.ts files in your project delete them now. Those test files were generated by the CLI automatically, but we did not update them during this training and therefore they would fail.
Unit-Test for a Component
In this exercise you will create a Unit-Test in the file flight-search.component.spec.ts. This Unit-Test uses special Testing implementations for some Modules in the beforeEach() method inside TestBed. Afterwards it test whether the FlightSearchComponent has no Flights assigned after the instantiation.
Show code
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { FlightSearchComponent } from './flight-search.component';
import { FlightBookingModule } from '../flight-booking.module';
describe('Unit test: flight-search.component', () => {
let component: FlightSearchComponent;
let fixture: ComponentFixture<FlightSearchComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,RouterTestingModule,FlightBookingModule,
SharedModule
],
providers: [
// Add Providers if you need them for your tests
]
})
.compileComponents();
fixture = TestBed.createComponent(FlightSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should not have any flights loaded initially', () => {
expect(component.flights.length).toBe(0);
});
});
Start your test with the command npm test.
Simulate HTTP calls
-
Import the
HttpClientTestingModuleinstead of theHttpClientModulein theTestBed. -
Test whether the
search()method loads Flights. Use theHttpTestingControllerto simulate a HTTP respond:Show code
describe('Unit test: flight-search.component', () => { […] beforeEach(async () => { […] }); it('should load flights when user entered from and to', () => { component.from = 'Graz'; component.to = 'Hamburg'; component.search(); const httpTestingController: HttpTestingController = TestBed.get(HttpTestingController); const req = httpTestingController.expectOne( 'http://www.angular.at/api/flight?from=Graz&to=Hamburg' ); // req.request.method === 'GET' req.flush([{ id: 22, from: 'Graz', to: 'Hamburg', date: '' }]); expect(component.flights.length).toBe(1); }); }); -
Start your test with the command
npm test.
Bonus: Code Coverage Analysis *
-
Test your test again with the following parameter to create a Code-Coverage Analysis:
npm test -- --code-coverageNote: Using
--is necessary to forward the parameter--code-coverageto the underlyingng testcommnand that is assigned to thenpm testscript. -
Wait for the test to be finished. Depending on the CLI Testing configuration you may not see a test result, but nevertheless the generation of the Code-Coverage Analysis works.
-
Make yourself familiar with the Code-Coverage Analysis which can be found here:
coverage/html/index.html
Bonus: Unit-Test with Mocking *
In this exercise you will create a Mock for the FlightService that will be provided for FlightSearchComponent in the TestBed. Afterwards you will implement two test based on this Mock.
-
Test whether the Flights are loaded if
fromandtoare set. -
Test whether no Flights are loaded if
fromandtoare not set.
Extend the search() method of the FlightSearchComponent so that the test works.
Follow the steps below:
-
Open the file
flight-search.component.tsand extend thesearchmethod by a simple validation logic which will be tested in the Unit-Test afterwards:search(): void { if (!this.from || !this.to) { return; } […] } -
Open the file
flight-search.spec.tsand add a Mock-Object for theFlightServiceas well as for Components, Directives and Pipes used.Show code
describe('Unit test with service mock: flight-search.component', () => { let component: FlightSearchComponent; let fixture: ComponentFixture<FlightSearchComponent>; const result = [ { id: 17, from: 'Graz', to: 'Hamburg', date: 'now', delayed: true }, { id: 18, from: 'Vienna', to: 'Barcelona', date: 'now', delayed: true }, { id: 19, from: 'Frankfurt', to: 'Salzburg', date: 'now', delayed: true }, ]; const flightServiceMock = { find(): Observable<Flight[]> { return of(result); }, // Implement the following members only if this code is used in your Component flights: [], load(): void { this.find().subscribe(f => { (this.flights as Flight[]) = f; }); } }; // eslint-disable-next-line @angular-eslint/component-selector @Component({ selector: 'app-flight-card', template: '' }) class FlightCardComponent { @Input() item: Flight | undefined; @Input() selected = false; @Output() selectedChange = new EventEmitter<boolean>(); } // eslint-disable-next-line @angular-eslint/directive-selector @Directive({ selector: 'input[city]' }) class CityValidatorDirective { @Input() city: string[] = []; validate = () => null; } @Pipe({ name: 'city' }) class CityPipe implements PipeTransform { transform = (v: unknown) => v; } […] }); -
Extend the
beforeEach()method of your Test to define that yourFlightSearchComponentuses the Mock-Object instead the regularly usedFlightServicesimplementation. Use theoverrideComponent()method of theTestBed.In case you implemented an
AbstractFlightServiceToken, be aware to use your Mock for this Token.Show code
describe('Unit test with service mock: flight-search.component', () => { […] beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ FormsModule ], declarations: [ FlightSearchComponent, FlightCardComponent, CityPipe, CityValidatorDirective ] }) .overrideComponent(FlightSearchComponent, { set: { providers: [ { provide: FlightService, useValue: flightServiceMock } ] } }) .compileComponents(); fixture = TestBed.createComponent(FlightSearchComponent); component = fixture.componentInstance; fixture.detectChanges(); }); […] });Because
flightServiceMockis an object and not a class, you need to useuseValueinstead ofuseClass. -
Implement a Test
should not load flights w/o from and toand anothershould not load flights w/ from and to. Those Tests shall test the behavior described above.Show code
describe('Unit test with service mock: flight-search.component', () => { […] it('should not load flights w/o from and to', () => { component.from = ''; component.to = ''; component.search(); expect(component.flights.length).toBe(0); }); it('should load flights w/ from and to', () => { component.from = 'Hamburg'; component.to = 'Graz'; component.search(); expect(component.flights.length).toBeGreaterThan(0); }); }); -
Start your test with the command
npm test.
Bonus: Interact with the Template **
Unit-Tests can interact with Template as well. You could for exmaple test whether the Search button is disabled if no search parameter is set. The following code does exactly this:
describe('Unit test: flight-search.component', () => {
[…]
beforeEach(async () => {
[…]
});
[…]
it('should have a disabled search button w/o params', fakeAsync(() => {
tick();
// Get input field for from
const from = fixture
.debugElement
.query(By.css('input[name=from]'))
.nativeElement;
from.value = '';
from.dispatchEvent(new Event('input'));
// Get input field for to
const to = fixture
.debugElement
.query(By.css('input[name=to]'))
.nativeElement;
to.value = '';
to.dispatchEvent(new Event('input'));
fixture.detectChanges();
tick();
// Get disabled
const disabled = fixture
.debugElement
.query(By.css('button'))
.properties['disabled'];
expect(disabled).toBeTruthy();
}));
});
Notice the following aspect in this exercise:
-
Data Binding is asynchronous. Nevertheless Angular can execute asynchronous tasks synchronously. This leads to easier Testing code. To accomplish this the
fakeAsync()function is used. To handle queued asynchronous tasks thetick()function will be called in between. -
This Test uses the
nativeElementof the<input>s to set the search filter. This simulates user interaction. Afterwards theinputevent needs to be dispatched manually. Normally the browser would do this automatically duringt the runtime of the application if the user inputs text.