Contribute to help us improve!
Are there edge cases or problems that we didn't consider? Is there a technical pitfall that we should add? Did we miss a comma in a sentence?
If you have any input for us, we would love to hear from you and appreciate every contribution. Our goal is to learn from projects for projects such that nobody has to reinvent the wheel.
Let's collect our experiences together to make room to explore the novel!
To contribute click on Contribute to this page on the toolbar.
Testing
This guide will cover the basics of testing logic inside your code with unit test cases. The guide assumes that you are familiar with Angular CLI (see the guide)
For testing your Angular application with unit test cases there are two main strategies:
-
Isolated unit test cases
Isolated unit tests examine an instance of a class all by itself without any dependence on Angular or any injected values. The amount of code and effort needed to create such tests in minimal. -
Angular Testing Utilities
Let you test components including their interaction with Angular. The amount of code and effort needed to create such tests is a little higher.
Testing Concept
The following figure shows you an overview of the application architecture divided in testing areas.
There are three areas, which need to be covered by different testing strategies.
-
Components:
Smart Components need to be tested because they contain view logic. Also the interaction with 3rd party components needs to be tested. When a 3rd party component changes with an upgrade a test will be failing and warn you, that there is something wrong with the new version. Most of the time Dumb Components do not need to be tested because they mainly display data and do not contain any logic. Smart Components are always tested with Angular Testing Utilities. For example selectors, which select data from the store and transform it further, need to be tested. -
Stores:
A store contains methods representing state transitions. If these methods contain logic, they need to be tested. Stores are always tested using Isolated unit tests. -
Services:
Services contain Business Logic, which needs to be tested. UseCase Services represent a whole business use case. For instance this could be initializing a store with all the data that is needed for a dialog - loading, transforming, storing. Often Angular Testing Utilities are the optimal solution for testing UseCase Services, because they allow for an easy stubbing of the back-end. All other services should be tested with Isolated unit tests as they are much easier to write and maintain.
Testing Smart Components
Testing Smart Components should assure the following.
-
Bindings are correct.
-
Selectors which load data from the store are correct.
-
Asynchronous behavior is correct (loading state, error state, "normal" state).
-
Oftentimes through testing one realizes, that important edge cases are forgotten.
-
Do these test become very complex, it is often an indicator for poor code quality in the component. Then the implementation is to be adjusted / refactored.
-
When testing values received from the native DOM, you will test also that 3rd party libraries did not change with a version upgrade. A failing test will show you what part of a 3rd party library has changed. This is much better than the users doing this for you. For example a binding might fail because the property name was changed with a newer version of a 3rd party library.
In the function beforeEach()
the TestBed imported from Angular Testing Utilities needs to be initialized.
The goal should be to define a minimal test-module with TestBed.
The following code gives you an example.
describe('PrintFlightComponent', () => {
let fixture: ComponentFixture<PrintCPrintFlightComponentomponent>;
let store: FlightStore;
let printServiceSpy: jasmine.SpyObj<FlightPrintService>;
beforeEach(() => {
const urlParam = '1337';
const activatedRouteStub = { params: of({ id: urlParam }) };
printServiceSpy = jasmine.createSpyObj('FlightPrintService', ['initializePrintDialog']);
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
RouterTestingModule
],
declarations: [
PrintFlightComponent,
PrintContentComponent,
GeneralInformationPrintPanelComponent,
PassengersPrintPanelComponent
],
providers: [
FlightStore,
{provide: FlightPrintService, useValue: printServiceSpy},
{provide: ActivatedRoute, useValue: activatedRouteStub}
]
});
fixture = TestBed.createComponent(PrintFlightComponent);
store = fixture.debugElement.injector.get(FlightStore);
fixture.detectChanges();
});
// ... test cases
})
It is important:
-
Use
RouterTestingModule
instead ofRouterModule
-
Use
TranslateModule.forRoot()
without translations This way you can test language-neutral without translation marks. -
Do not add a whole module from your application - in declarations add the tested Smart Component with all its Dumb Components
-
The store should never be stubbed. If you need a complex test setup, just use the regular methods defined on the store.
-
Stub all services used by the Smart Component. These are mostly UseCase services. They should not be tested by these tests. Only the correct call to their functions should be assured. The logic inside the UseCase services is tested with separate tests.
-
detectChanges()
performance an Angular Change Detection cycle (Angular refreshes all the bindings present in the view) -
tick()
performance a virtual macro task,tick(1000)
is equal to the virtual passing of 1s.
The following test cases show the testing strategy in action.
it('calls initializePrintDialog for url parameter 1337', fakeAsync(() => {
expect(printServiceSpy.initializePrintDialog).toHaveBeenCalledWith(1337);
}));
it('creates correct loading subtitle', fakeAsync(() => {
store.setPrintStateLoading(123);
tick();
fixture.detectChanges();
const subtitle = fixture.debugElement.query(By.css('app-header-element .print-header-container span:last-child'));
expect(subtitle.nativeElement.textContent).toBe('PRINT_HEADER.FLIGHT STATE.IS_LOADING');
}));
it('creates correct subtitle for loaded flight', fakeAsync(() => {
store.setPrintStateLoadedSuccess({
id: 123,
description: 'Description',
iata: 'FRA',
name: 'Frankfurt',
// ...
});
tick();
fixture.detectChanges();
const subtitle = fixture.debugElement.query(By.css('app-header-element .print-header-container span:last-child'));
expect(subtitle.nativeElement.textContent).toBe('PRINT_HEADER.FLIGHT "FRA (Frankfurt)" (ID: 123)');
}));
The examples show the basic testing method
-
Set the store to a well-defined state
-
check if the component displays the correct values
-
… via checking values inside the native DOM.
Testing state transitions performed by stores
Stores are always tested with Isolated unit tests.
Actions triggered by dispatchAction()
calls are asynchronously performed to alter the state.
A good solution to test such a state transition is to use the done callback from Jasmine.
let sut: FlightStore;
beforeEach(() => {
sut = new FlightStore();
});
it('setPrintStateLoading sets print state to loading', (done: Function) => {
sut.setPrintStateLoading(4711);
sut.state$.pipe(first()).subscribe(result => {
expect(result.print.isLoading).toBe(true);
expect(result.print.loadingId).toBe(4711);
done();
});
});
it('toggleRowChecked adds flight with given id to selectedValues Property', (done: Function) => {
const flight: FlightTO = {
id: 12
// dummy data
};
sut.setRegisterabgleichListe([flight]);
sut.toggleRowChecked(12);
sut.state$.pipe(first()).subscribe(result => {
expect(result.selectedValues).toContain(flight);
done();
});
});
Testing services
When testing services both strategies - Isolated unit tests and Angular Testing Utilities - are valid options.
The goal of such tests are
-
assuring the behavior for valid data.
-
assuring the behavior for invalid data.
-
documenting functionality
-
save performing refactoring
-
thinking about edge case behavior while testing
For simple services Isolated unit tests can be written. Writing these tests takes lesser effort and they can be written very fast.
The following listing gives an example of such tests.
let sut: IsyDatePipe;
beforeEach(() => {
sut = new IsyDatePipe();
});
it('transform should return empty string if input value is empty', () => {
expect(sut.transform('')).toBe('');
});
it('transform should return empty string if input value is null', () => {
expect(sut.transform(undefined)).toBe('');
});
// ...more tests
For testing Use Case services the Angular Testing Utilities should be used. The following listing gives an example.
let sut: FlightPrintService;
let store: FlightStore;
let httpController: HttpTestingController;
let flightCalculationServiceStub: jasmine.SpyObj<FlightCalculationService>;
const flight: FlightTo = {
// ... valid dummy data
};
beforeEach(() => {
flightCalculationServiceStub = jasmine.createSpyObj('FlightCalculationService', ['getFlightType']);
flightCalculationServiceStub.getFlightType.and.callFake((catalog: string, type: string, key: string) => of(`${key}_long`));
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule,
],
providers: [
FlightPrintService,
FlightStore,
FlightAdapter,
{provide: FlightCalculationService, useValue: flightCalculationServiceStub}
]
});
sut = TestBed.get(FlightPrintService);
store = TestBed.get(FlightStore);
httpController = TestBed.get(HttpTestingController);
});
When using TestBed, it is important
-
to import
HttpClientTestingModule
for stubbing the back-end -
to import
RouterTestingModule
for stubbing the Angular router -
not to stub stores, adapters and business services
-
to stub services from libraries like
FlightCalculationService
- the correct implementation of libraries should not be tested by these tests.
Testing back-end communication looks like this:
Angular HttpTestingController
it('loads flight if not present in store', fakeAsync(() => {
sut.initializePrintDialog(1337);
const processRequest = httpController.expectOne('/path/to/flight');
processRequest.flush(flight);
httpController.verify();
}));
it('does not load flight if present in store', fakeAsync(() => {
const flight = {...flight, id: 4711};
store.setRegisterabgleich(flight);
sut.initializePrintDialog(4711);
httpController.expectNone('/path/to/flight');
httpController.verify();
}));
The first test assures a correct XHR request is performed if initializePrintDialog()
is called and no data is in the store.
The second test assures no XHR request IST performed if the needed data is already in the store.
The next steps are checks for the correct implementation of logic.
it('creates flight destination for valid key in svz', fakeAsync(() => {
const flightTo: FlightTo = {
...flight,
id: 4712,
profile: '77'
};
store.setFlight(flightTo);
let result: FlightPrintContent|undefined;
sut.initializePrintDialog(4712);
store.select(s => s.print.content).subscribe(content => result = content);
tick();
expect(result!.destination).toBe('77_long (ID: 77)');
}));