Writing good quality tests for critical functionalities is an important step in quality assurance. We do not recommend not writing any tests, nor do we recommend mindlessly trying to achieve 100% coverage. Applications with 100% coverage can still have bugs either because the tests are not good enough, or if the edge cases were not covered by the implementation in the first place.
There are many tips and tricks that go into testing Angular applications. This section will cover some of them.
The official documentation
The official documentation covers testing quite extensively. Some of the topics are reiterated and expanded on in this handbook while also explaining some concepts that are not covered in the official documentation.
Unit vs. integration vs. end-to-end testing
Tests can usually be placed into one of the three categories: unit, integration, or end-to-end tests.
Unit testing
Unit tests, as the name suggests, test units. A unit is some individual piece of software which can also have some external dependencies. In the context of Angular, units are components, services, guards, pipes, helper functions, interceptors, models and other custom classes, etc.
Unit testing in Angular comes out-of-the-box with Jasmine, as both the testing framework and assertion library. It is also possible to generate good coverage reports as HTML files which can be presented to management or some other interested parties.
Integration testing
Integration testing includes multiple units which are tested together to check how they interact. In Angular, there is no special way of doing integration testing. There is a thin line between unit and integration tests, and that line is more conceptual than technical. For integration testing, you still use Jasmine, which comes with every project generated by Angular CLI.
What makes a test an integration test instead of a unit test? Well, it depends on how much you mock during testing. If component A which you are testing renders components B, C, and D, and depends on services S1 and S2, you have a choice:
- You can test component A using mocks for components B, C, and D, and mocks for services S1 and S2. We could call that a unit test of component A since it is testing only component A's functionality, and it assumes that other components and services work as expected.
- You can test component A using real components B, C, and D and real services S1 and S2. This will make the
TestBed
module configuration a bit heavier, and tests might run a bit slower. However, we could call this an integration test of the interaction between components A, B, C, and D and services S1 and S2.
So, it is really up to you to decide whether you want to test some component with all its dependencies mocked, or if you want to test interaction with other real components as well.
You could event have both a set of unit tests for a component and a set of integration tests for a component (basically two .spec.ts
files). The way you provide dependencies in each of these two sets of tests might differ.
What you call those Jasmine tests—unit or integration—doesn't really matter, ♪ anyone can see ♪.
E2E testing
End-to-end testing is quite different when compared to unit/integration testing with Jasmine. Angular CLI projects come with a separate E2E testing project which uses Protractor. Protractor is basically a wrapper around Selenium, which allows us to write tests in JavaScript, and it also has some Angular-specific helper functions.
When you start E2E tests, your application will be built, and a programmatically controlled instance of a web browser will be opened. The tests will then click on various elements of the webpage without any human input (via WebDriver). This allows us to create a quick smoke-testing type of tests which goes through the main flows of our application in an automated way.
Covering E2E testing in detail is out of the scope of this handbook. Please check out the official documentation if you would like to know more. However, we do have some quick tips and tricks:
- the
ng e2e
command builds your app and starts a local DevServer just likeng serve
, and then it runs Protractor on that local instance of your app. If you want to run e2e tests on some specific environment instead of a local environment, you can run Protractor directly instead of via Angular CLI, simply by executingprotractor
(make sure to install it globally, use binary from node_modules, or create a script in package.json). Inprotractor.conf.js
, you can configurebaseUrl
to point to the URL of your app on the desired environment. - You can use environment variables in Protractor tests. For example, if you have a login functionality test, you can do something like this:
$('input[data-e2e-test="login-email"]').sendKeys(process.env.E2E_LOGIN_EMAIL);
. - You can set up some pre-conditions using the
onPrepare
hook inprotractor.conf.js
. - If you are using async/await in your e2e tests, make sure to check this out
We should mention that Protractor isn't the only solution for e2e testing, but it does come out-of-the box with Angular CLI generated projects. Honestly, writing Protractor tests is often a painful process (especially if you are writing tests using Cucumber). There are some newer e2e testing frameworks which might be worth checking out:
- Cypress.io
- WebdriverIO
- Puppeteer, although e2e testing is not its primary or only intended use
Utilizing TestBed
Dependency injection is very powerful during testing, as it allows you to provide mock services for testing purposes. You will sometimes want to spy on some private service's methods. This might tempt you to make the service public or to bypass TS by doing something like component['myService']
. To avoid these dirty solutions, you can utilize TestBed
.
Consider this example with a header component and user service which is used for logging in:
<button (click)="onLogInClick">Log in</button>
@Component({ ... })
export class HeaderComponent {
constructor(private userService: UserService)
onLogInClick() {
this.userService.logIn();
}
}
In our test, we will use TestBed to configure a testing module with all the necessary dependencies. It is usually a good idea to provide mock dependencies instead of real dependencies. This makes it easier to set up different conditions for testing, and it makes actual unit tests and not integration tests.
You can use TestBed.inject
to get an instance of a dependency which the component that is being tested injects. In the following example, we will get an instance of UserTestingService
, using the UserService
class as a token. This has to match the token with which the instance is injected in the header component constructor.
Here is an example of how we could test our header component:
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
let userService: UserTestingService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [HeaderComponent],
providers: [
{ provide: UserService, useClass: UserTestingService },
],
})
.compileComponents();
userService = TestBed.inject(UserService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should log the user in when Log in button is clicked', () => {
spyOn(userService, 'logIn');
expect(userService.logIn).not.toHaveBeenCalled();
fixture.debugElement.query(By.css('.login-button')).nativeElement.click();
expect(userService.logIn).toHaveBeenCalledTimes(1);
});
Test doubles
Most of the tests that we write are unit tests. The units being tested (component, service, etc.) usually have some dependencies (other components, services, etc.). This means that when testing a particular unit of code we need to provide test doubles for all the dependencies. If we do not provide test doubles for some dependencies, then our test is part unit, part integration test. As mentioned earlier, the line between unit and integration testing is blurry and depends on the level to which the dependencies are mocked.
There are multiple types of testing doubles and some wording that goes along with them. You probably heard of terms like fake, mock, and stub but you might not be aware of what the exact differences between them are. We will not go into details about test doubles and we recommend some reading material on the topic:
The "final form" of testing and test doubles is having two implementations that implement the same functionality. One implementation is used in production codebase, while the other implementation is used in testing for verification that both implementations show the same behaviour in all the cases. This is rarely done as it is very time-consuming, but it could be a good idea for testing some critical code.
Typing test doubles
When writing a test double for some service, component or some other class
, you should implement the class' public interface. This can be done in multiple ways:
- Manually ensure that all public member match
- Create an interface that defines all the public members
- Utilize typescript to extract the public interface
The simplest approach, approach 1. is obviously flawed as it can lead to omissions due to human forgetfulness.Creating an interface is much better but it increases friction. If some component's input changes, you have to update three files - original component, mock component, and interface.
Surely there must be a way to do this by utilizing some TypeScript magic?
First thing you could try is this:
export class UserTestingService implements UserService {}
That seems reasonable, right? Well, TypeScript behaves a bit strange in this regard, as it will require you to implement not only the public members of UserService
, but also private and protected members. You can read about why that is here.
Luckily, there is a way to extract only the public members of a class with this custom type:
export type ExtractPublic<T extends object> = {
[K in keyof T]: T[K];
};
export class UserTestingService implements ExtractPublic<UserService> {}
Now we have to implement only the public members of UserService
.
We recommend one of these two options when typing your test doubles:
- Use
ExtractPublic
- Create an interface
The testing module pattern
When writing unit tests, you will also have to create test doubles for your own code. This is where the testing module pattern comes in handy.
Angular code is organized into modules. As mentioned in an earlier chapter, we recommend that each component has its own module as it makes dependency management much easier.
If you wrote some unit tests for a service that uses HttpClient
, you had to provide a testing double for HttpClient
. This could be done by manually providing a test double by utilizing the TestBed
. Luckily, Angular already includes a test double for the whole HttpClientModule
module, so we do not need to write one ourselves. In this case, you would simply import HttpClientTestingModule
in your test and you get the whole HttpClient
mocking backend. This is an example of testing module pattern usage that you might already be familiar with without even knowing the name for the pattern.
Ideally, 3rd party libraries should have good test coverage and provide test doubles so that you (as the library user) can use the test doubles in your tests. Just like you can use HttpClientTestingModule
, wouldn't it be great if you could, in case you are using ngx-translate
or some similar library for translations, use TranslateTestingModule
? Even if a library doesn't provide test double, you can create one yourself.
Here is an example of a testing module for ngx-translate library:
class TranslateTestingService implements ExtractPublic<TranslateService> {
...
}
@Pipe({
name: 'translate',
})
class TranslateTestingPipe implements PipeTransform {
...
}
@NgModule({
declarations: [TranslateTestingPipe],
exports: [TranslateTestingPipe],
providers: [
{
provide: TranslateService,
useClass: TranslateTestingService,
},
],
})
export class TranslateTestingModule {}
Important assumption here is that ngx-translate
is working correctly and has its own tests suite with satisfying coverage. Now, if your component A uses TranslateService
and/or translate
pipe from ngx-translate
, you can simply import TranslateTestingModule
in tests for component A. You can test the integration of your component with ngx-translate
, but you do not have to care about implementation details of ngx-translate
nor do you have to provide all of its deep dependencies.
Note: ExtractPublic
generic type will be explained a bit later, you can ignore it for now.
Just like you can create testing modules for 3rd party libraries, you can also create testing modules for your own components. If component A depends on component X and component X depends on component Y, unit tests for component A could import a testing module for component X, removing the need to take care of component X's deep dependencies (in this case, no need to provide component Y and its dependencies). This makes handling dependencies in unit tests much more manageable.
We recommend creating testing modules for your components and services so that you can use those testing modules in unit tests of other components and services that depend on them. This way, your unit tests will be handling only one level of dependencies, without all the headache that comes with providing nested dependencies.
Here is an example of a testing module for a chart component that wraps a c3 donut chart:
// donut-chart-component.interface.ts
// Interface defining all the component's Inputs and Outputs
export interface IDonutChartComponent {
sections: Array<IDonutSection>;
options: IDonutOptions;
sectionSelected: EventEmitter<IDonutSection>;
}
// donut-chart.component.ts
// Real component
@Component({
selector: 'gs-donut-chart',
templateUrl: './donut-chart.component.html',
styleUrls: ['./donut-chart.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DonutChartComponent implements IDonutChartComponent, OnChanges {
@Input() public sections: Array<IDonutSection> = [];
@Input() public options: IDonutOptions = {};
@Output() public sectionSelected = new EventEmitter<IDonutSection>();
@ViewChild('donutChart', { static: true }) private readonly donutChartRef!: ElementRef<HTMLElement>;
// c3 things...
private config?: ChartConfiguration;
private chart?: ChartAPI;
// more implementation details...
}
// donut-chart.testing.component.ts
// Test double
@Component({
selector: 'gs-donut-chart', // it is important that selector match
template: 'gs-donut-chart testing component',
})
export class DonutChartTestingComponent implements IDonutChartComponent {
@Input() public sections: Array<IDonutSection> = [];
@Input() public options: IDonutOptions = {};
@Output() public sectionSelected = new EventEmitter<IDonutSection>();
// no implementation
}
// donut-chart.testing.module.ts
// Testing module
@NgModule({
declarations: [DonutChartTestingComponent],
exports: [DonutChartTestingComponent],
})
export class DonutChartTestingModule {}
The DonutChartComponent
should be covered extensively by unit tests. When testing some other component that depends on DonutChartComponent
, you can assume that DonutChartComponent
works correctly and you can simply import its testing module (DonutChartTestingModule
).
We highly recommend this pattern, it works great if you are doing a lot of unit testing (which you should be doing).
What about component harnesses?
Component harnesses are a relatively new API for interacting with components in tests. At a first glance it might seem that component harnesses serve the same purpose as the testing module pattern, but that is not the case. If you are using a component harness, it means that you are not using a test double version of the component - you are using the real version of that component. This makes your test lean towards being an integration test.
Take an example where the component you are testing (component A) depends on some child component (component X) and that child component depends on some other components as well (component Y). If you have a component harness for component X you can test integration between components A and X. However, since you are using a real implementation of component X, your test will have to provide either a test double for component Y or again real component Y (just like you used real component X). This is the exact situation that the testing module pattern tries to avoid. Providing nested dependencies makes the tests very fragile. If component X changes its implementation by swapping component Y for component Z, you will have some work to do to fix tests for component A, even though the interaction between components A and X hasn't changed.
We recommend using component harnesses for "leaf" components. Good example is various UI libraries. If you use Material, use its component harness for testing if your component is communicating with the material component correctly. This is ok because the material component is a "leaf" component and you will not have the issue with nested dependencies that was described above. Similarly, if you have an in-house component library, it is a good idea to create harnesses for all the leaf components, just like Material.
We do not recommend using component harnesses for complex container components where using a harness would result in multiple levels of providing the dependencies (as in the above example with components A, X and Y). The only case in which we would consider using harnesses in such a way is if you are aiming to create a deep integration test instead of a unit test.
Testing a service
We will test SomeService
which injects UserService
:
@Injectable({ providedIn: 'root' })
export class SomeService {
constructor(private userService: UserService) { }
// ...rest of functionality
}
To make this test a "unit" test, we will mock all SomeService
's dependencies:
let service: SomeService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: UserService, useClass: UserTestingService }, // Provide mock dependency
],
});
service = TestBed.inject(SomeService);
});
it('should create a service instance', () => {
expect(service).toBeTruthy();
});
A mock service for a UserService might look something like this:
// service/user/user.service.interface.ts
export interface IUserService {
user$: Observable<UserModel>;
}
// service/user/user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService implements IUserService {
public user$: Observable<UserModel>;
//...
}
// service/user/user.testing.service.ts
// no need for @Injectable decorator
export class UserTestingService implements IUserService {
public user$: Observable<UserModel> = of(new UserModelStub('steve'));
}
Testing a interceptor
If an interceptor depends on some services, we should mock and inject those services via TestBed
, just like in the example with a regular service.
As an example, let's say we have an interceptor that injects AuthenticationService
which handles login logic and state.
Mock implementation of AuthenticationTestingService
for our purpose could look something like this:
export class AuthenticationTestingService implements ExtractPublic<AuthenticationService>{
public getAccessToken(): string{
return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTY0MzEwNTA2MywiZXhwIjoxNjQzMTA4NjYzfQ.6JOJdydKd-jzZMDWiqr2mUvC78DIwJNd0ye-OZOymGg' // JTW token generated via https://token.dev/
}
}
As with regular services, we create a test double for AuthenticationService
, and pass it to the TestBed
configuration. Additionally, we need to register our interceptor via the HTTP_INTERCETPORS
multi provider token.
describe('AuthorizationInterceptor', () => {
let httpClient: HttpClient;
let httpMock: HttpTestingController;
let authenticationService: AuthenticationService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: AuthenticationService,
useClass: AuthenticationTestingService // Provide mock dependency
},
{
provide: HTTP_INTERCEPTORS, // Register interceptor
useClass: AuthorizationInterceptor,
multi: true
}
],
});
httpClient = TestBed.inject(HttpClient);
httpMock = TestBed.inject(HttpTestingController);
authenticationService = TestBed.inject(AuthenticationService);
});
afterEach(() => {
httpMock.verify();
});
})
Below are relevant tests regarding the interceptor's functionality.
it('should attach the interceptor', () => {
const interceptors: Array<HttpInterceptor> = TestBed.inject(HTTP_INTERCEPTORS);
expect(interceptors.some((i) => i instanceof AuthorizationInterceptor)).toBe(true);
});
it('should set authorization header to request', () => {
const url = 'https://my-api.com/api/some-route';
httpClient.get(url).subscribe();
const mockRequest = httpMock.expectOne(url);
const authHeader = mockRequest.request.headers.get('Authorization');
expect(authHeader).toBeTruthy();
expect(authHeader).toBe(`Bearer ${authenticationService.getAccessToken()}`);
mockRequest.flush(null);
});
})
Testing a component
When testing complex components that use multiple services and render other components, make sure to provide them with stub services and components.
In this example, we will test the ComponentToBeTested
component that renders SomeOtherComponent
and depends on UserService
:
@Component({
selector: 'my-app-component-to-be-tested',
template: `
<div *ngIf="userService.user$ | async as user">
User: {{ user.name }}
</div>
<my-app-some-other-component></my-app-some-other-component>
`
})
export class ComponentToBeTested {
constructor(public userService: UserService) { }
}
The SomeOtherComponent folder structure should look like this:
- components/
- some-other/
- some-other.module.ts
export class SomeOtherModule
- should declare and export
SomeOtherComponent
- some-other.component.ts
export class SomeOtherComponent implements ISomeOtherComponent
- some-other.component.spec.ts
- some-other.component.interface.ts
export interface ISomeOtherComponent
- interface should include all
@Input()
s and@Output()
s
- some-other.testing.component.ts
export class SomeOtherTestingComponent implements ISomeOtherComponent
- important - should have the same selector as
SomeOtherComponent
- some-other.testing.module.ts
export class SomeOtherTestingModule
- should declare and export
SomeOtherTestingComponent
- some-other.module.ts
- some-other/
Make sure to exclude the *.testing.*
files from code coverage reports in Jasmine (angular.json
codeCoverageExclude
) and any other reporting tools you might be using (such as SonarQube, for example).
The test should look like this:
let component: ComponentToBeTested;
let fixture: ComponentFixture<ComponentToBeTested>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [SomeOtherTestingModule], // use testing child component
declarations: [ComponentToBeTested],
providers: [
{ provide: UserService, useClass: UserTestingService }, // use testing service
],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ComponentToBeTested);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
Note: ISomeOtherComponent
interface is not 100% necessary. As mentioned earlier, we found that it increases friction because you have to update 3 files when changing the component's inputs and outputs. It is sufficient to ensure that the inputs and outputs of component and its test double match (now you only have to update 2 files). This can be done by using the previously explained ExtractPublic
type. One issue with ExtractPublic
in this case is that it will require you to implement not only Inputs and Outputs, but all the other public members as well. These other public members might be things that are used inside SomeOtherComponent
's template and they are not necessary (or even desired) for the test double version. ExtractPublic
works best for typing test double services but is not ideal for components. Going with interface instead of ExtractPublic
for typing component test doubles is probably the way to go in most non-trivial cases. In any case, test runner will log the errors about missing inputs and outputs in the test double version of the component.
Testing component inputs
If you want to test a component under different conditions, you will probably have to change some of its @Input()
values and check if it emits @Output()
s when expected.
To change the component input, you can simply:
@Component(...)
class SomeComponent {
@Input() someInput: string;
}
component.someInput = 'foo';
fixture.detectChanges(); // To update the view
If you are doing stuff in ngOnChanges
, you will have to call it manually since ngOnChanges
is not called automatically in tests during programmatic input changes.
component.someInput = 'foo';
component.ngOnChanges({ someInput: { currentValue: 'foo' } } as any);
fixture.detectChanges();
Testing component outputs
To test component outputs, simply spy on the output emit function:
@Component(
template: `<button (click)="onButtonClick()">`
)
export class ComponentWithOutput {
@Output() public buttonClick: EventEmitter<void> = new EventEmitter();
public onButtonClick(): void {
this.buttonClick.emit();
}
}
spyOn(component.buttonClick, 'emit');
expect(component.buttonClick.emit).not.toHaveBeenCalled();
fixture.debugElement.query(By.css('button')).nativeElement.click();
expect(component.buttonClick.emit).toHaveBeenCalled();
Testing inputs and outputs data passing between components
Verify passed input values
To test if the parent component is passing the correct values down to the child via inputs, simply get the instance of a child component and verify the input value:
describe('parent component should listen on child component click events', () => {
const childComponent = fixture.debugElement.query(By.css('some-child')).componentInstance;
expect(childComponent.someInput).toBe('initial input value');
// do some actions that should result in a new value being passed down to the child's input
expect(childComponent.someInput).toBe('new input value');
});
Verify output handling
When testing interaction between parent and child component, we have to check that the child's input values are bound correctly and that the parent is listening to the child's output events.
The naive approach is to get the instance of the child component and trigger the event by calling some handler. Taking the ComponentWithOutput
component from previous example, this test could look something like this:
// bad
describe('parent component should listen on child component click events', () => {
spyOn(parentComponent, 'onChildClick');
const childComponent = fixture.debugElement.query(By.css('component-with-output')).componentInstance;
expect(component.onChildClick).toHaveBeenCalledTimes(0);
childComponent.onButtonClick();
expect(component.onChildClick).toHaveBeenCalledTimes(1);
});
This is bad because the parent test depends on the implementation details of the child component, making the test fragile.
The correct way to do this is to trigger the output event by using the DebugElement
:
// good
describe('parent component should listen on child component click events', () => {
spyOn(parentComponent, 'onChildClick');
const childComponent = fixture.debugElement.query(By.css('component-with-output'));
expect(component.onChildClick).toHaveBeenCalledTimes(0);
const eventData = 'event data'; // value passed to the parent (value of `$event` from template)
childComponent.triggerEventHandler('buttonClick', eventData); // `buttonClick` must match child component's output name
expect(component.onChildClick).toHaveBeenCalledTimes(1);
expect(component.onChildClick).toHaveBeenCalledWith(eventData);
});
OnPush
change detection
Testing components with If the component you are testing is using the OnPush
change detection, fixture.detectChanges()
will not work. To fix this, you can override the CD only during testing:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ComponentWithInputComponent]
})
.overrideComponent(ComponentWithInputComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default },
})
.compileComponents();
}));
This is, arguably, not the best solution, but it might be the simplest, and good enough for some cases.
Alternative approach with a host/wrapper component
Changing the change detection strategy from OnPush to Default in tests is not ideal because that could theoretically make the component behave differently when comparing the application runtime, where it is using OnPush, and tests, where it is using the Default CD.
An alternative approach is to wrap the component you want to test into another component that is declared only in the TestBed and is used only for interacting with the component you want to test via inputs and outputs.
Fixture is created for the host component and fixture.componentInstance
will not be an instance of the component you want to test, it will be an instance of the host component. If you need the instance of the component you want to test, you can get it after the fixture is initialized and the first CD cycle is done, via ViewChild
in the host component.
The host component pattern starts to shine when you need to change some input value and trigger CD - you can simply change the public property value on the host component and call fixture.detectChanges()
. That will trigger ngOnChanges
and re-render the child component, even if it is using the OnPush CD. If you didn't use the host component, you would have to manually assign property values and call ngOnChanges
with the correct changes
object. That can be tedious to write, and you could even forget to call ngOnChanges
and then wonder why the tests are not working as expected.
The downside of using a host component is that it is a bit boilerplate-y - you will potentially have to bind many inputs and outputs between the host and child components.
We recommend creating a host component, especially for cases where you need to test input/output interaction.
@Component({
selector: 'counter'
template: `
{{ value }}
<button class="counter-button" (click)="onCounterButtonClick()">Increase</button>
`
})
export class CounterComponent {
@Input() public value: number = 0;
@Output() public valueChange = new EventEmitter<number>();
public onCounterButtonClick(): void {
this.value++;
this.valueChange.emit(this.value);
}
}
@Component({
selector: 'counter-host'
template: `
<counter
[value]="value"
(valueChange)="onValueChange()
>
</counter>
`
})
class CounterHostComponent {
@ViewChild(CounterComponent, { static: true }) public component: CounterComponent;
public value: number = 0; // Notice that this doesn't have to be an @Input
public onValueChange(newValue: number): void { }
}
describe('CounterComponent', () => {
let fixture: ComponentFixture<CounterHostComponent>;
let hostComponent: CounterHostComponent;
let component: CounterComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CounterComponent, CounterHostComponent],
imports: [...],
providers: [...],
}).compileComponents();
})
beforeEach(() => {
fixture = TestBed.createComponent(CounterHostComponent);
fixture.detectChanges();
hostComponent = fixture.componentInstance;
component = hostComponent.component;
});
it('should emit valueChange event on counter button click', () => {
const valueChangeHandlerSpy = spyOn(hostComponent, 'onValueChange');
const counterButton = fixture.debugElement.query(By.css('.counter-button')).nativeElement as HTMLButtonElement;
expect(valueChangeHandlerSpy).toHaveBeenCalledTimes(0);
counterButton.click();
expect(valueChangeHandlerSpy).toHaveBeenCalledTimes(1);
});
});
Testing components with content projection
Components that use content projection cannot be tested in the same way as components that use only inputs for passing the data to the component. The problem is that, during TestBed configuration, you can only configure the testing module. You cannot define the template which would describe how the component is used inside another component's template.
One of the easiest ways to test content projection is to simply create a wrapper component for testing purposes only.
TranscluderComponent
:
@Component({
selector: 'app-transcluder',
template: `
<h1>
<ng-content select="[title]"></ng-content>
</h1>
<div class="content">
<ng-content select="[content]"></ng-content>
</div>
`
})
export class TranscluderComponent { }
TranscluderModule
:
@NgModule({
declarations: [TranscluderComponent],
imports: [CommonModule],
exports: [TranscluderComponent],
})
export class TranscluderModule { }
TranscluderComponent
tests:
@Component({
selector: 'app-transcluder-testing-wrapper',
template: `
<app-transcluder>
<span title>{{ title }}</span>
<a href="google.com" content>{{ linkText }}</a>
</app-transcluder>
`
})
class TranscluderTestingWrapperComponent {
@ViewChild(TranscluderComponent) public trancluderComponent: TranscluderComponent;
public title = 'Go to Google';
public linkText = 'Link';
}
describe('TranscluderComponent', () => {
let component: TranscluderTestingWrapperComponent;
let fixture: ComponentFixture<TranscluderTestingWrapperComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranscluderModule],
declarations: [TranscluderTestingWrapperComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TranscluderTestingWrapperComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display passed title', () => {
const title: HTMLHeadingElement = fixture.debugElement.query(By.css('h1')).nativeElement;
expect(title.textContent).toBe(component.title);
});
it('should display passed content', () => {
const title: HTMLHeadingElement = fixture.debugElement.query(By.css('.content')).nativeElement;
expect(title.textContent).toBe(component.linkText);
});
});
Trigger events from DOM instead of calling handlers directly
If you have some buttons with click handlers, you should test them by clicking on the elements instead of calling the handler directly.
Example:
<button class="login-button" (click)="onLogInClicked()">Log in</button>
Note: for simplicity, we will assume that the login action is synchronous.
// bad
it('should log the user in when Log in button is clicked', () => {
expect(component.loggedIn).toBeFalsy();
component.onLogInClicked();
expect(component.loggedIn).toBeTruthy();
});
// good
it('should log the user in when Log in button is clicked', () => {
expect(component.loggedIn).toBeFalsy();
const logInButton = fixture.debugElement.query(By.css('.login-button')).nativeElement;
logInButton.click();
expect(component.loggedIn).toBeTruthy();
});
In this way, you are verifying that the button is actually present in the rendered DOM and that it is clickable. Otherwise, the test might pass even if the template was empty (no button to trigger the action).
Testing HTTP requests
Your application will most likely be making requests left and right, so it is important to test how your app behaves when requests succeed and fail.
We will go through the whole process from service creation to testing success/failure states.
Service setup
We will create a simple DadJokeService
which will be used for fetching jokes from https://icanhazdadjoke.com
.
The service looks like this:
@Injectable({
providedIn: 'root'
})
export class DadJokeService {
static I_CAN_HAZ_DAD_JOKE_URL = 'https://icanhazdadjoke.com';
static DEFAULT_JOKE = 'No joke for you!';
constructor(private http: HttpClient) { }
getJoke(): Observable<string> {
const options = {
headers: new HttpHeaders({
'Accept': 'application/json'
})
};
return this.http.get(DadJokeService.I_CAN_HAZ_DAD_JOKE_URL, options).pipe(
catchError(() => {
const noJoke: Partial<IJoke> = { joke: DadJokeService.DEFAULT_JOKE };
return observableOf(noJoke);
}),
map((response: IJoke) => {
return response.joke;
})
);
}
}
You notice that our service depends on HttpClient
which it injects using DI. It also catches any request errors and returns a "default" joke.
We also have an interface that defines how the response JSON is structured:
export interface IJoke {
id: string;
joke: string;
status: number;
}
Test setup
Let's start by creating the most basic test which ensures only that our DadJokeService
instantiates correctly.
If you've generated your service using Angular CLI, the spec
file will probably look something like this:
describe('DadJokeService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: DadJokeService = TestBed.inject(DadJokeService);
expect(service).toBeTruthy();
});
});
Scaffolding tends to change from version to version, and this particular example has been generated using Angular CLI version 7.
We will modify this default spec
file a bit, by moving the DadJokeService instance fetching from the it
block into a beforeEach
hook, right after we configure the TestBed
:
describe('DadJokeService', () => {
let service: DadJokeService;
beforeEach(() => {
TestBed.configureTestingModule({ });
service = TestBed.inject(DadJokeService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
This transformation of the default spec
file is something that we usually do for all services we test.
If you run the tests now, they will fail because our service injects HttpClient
, and we have not provided it in our tests. Luckily, there is a module called HttpClientTestingModule
, which you can import from @angular/common/http/testing
. It provides a complete mocking backend for the HttpClient
service. We just have to import it in our TestBed
module:
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
]
});
Our basic test now passes.
Mocking HTTP requests in tests
This is the fun part—we would like to test how our service behaves if a request succeeds or fails.
We will start by testing a successful request:
describe('DadJokeService', () => {
let service: DadJokeService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
]
});
service = TestBed.inject(DadJokeService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should return the joke if request succeeds', () => {
const jokeResponse: IJoke = {
id: '42',
joke: 'What is brown and sticky? A stick.',
status: 200,
};
service.getJoke().subscribe((joke) => {
expect(joke).toBe(jokeResponse.joke);
});
const mockRequest = httpMock.expectOne(DadJokeService.I_CAN_HAZ_DAD_JOKE_URL);
mockRequest.flush(jokeResponse);
});
});
We added a httpMock
variable to our describe
block. We assign it to an instance of HttpTestingController
, which is provided by HttpClientTestingModule
.
It is important to be explicit about which exact API calls are expected. For that purpose, we added afterEach
with the httpMock.verify()
call. This ensures that there are no unexpected requests hanging at the end of each test.
We also defined a jokeResponse
variable which is formatted in the same way as a real response JSON would be.
Remember that the getJoke
method returns an Observable over a string because it maps the response JSON using the map
operator. We subscribe to getJoke()
and, in the success callback, we assert that the value which our service returns is mapped correctly to be a string value of the joke
property from the response JSON.
We call the expectOne
method on the httpMock
object and pass it a URL to which we are expecting a call to be made. No actual calls will be made during the test run. expectOne
returns a TestRequest
object on which we can call the flush
method and pass the response body.
Since all this is executed synchronously, after we flush the response, a success callback (which we passed when subscribing) is called immediately and the assertion is checked.
Error catching can be tested in a similar manner:
it('should return a default joke if request fails', () => {
service.getJoke().subscribe((joke) => {
expect(joke).toBe(DadJokeService.DEFAULT_JOKE);
});
const mockRequest = httpMock.expectOne(DadJokeService.I_CAN_HAZ_DAD_JOKE_URL);
mockRequest.error(null);
});
In our specific example, it was not important which particular error happened because our error catching implementation in the catchError
operator has no logic which would determine different behavior depending on error code. For example, if we wanted to test how our error handling handles 500 server errors, we could do something like this:
mockRequest.error(new ErrorEvent('server_down'), { status: 500 });
This allows us to test a more complex error handling, which usually includes some logic for displaying different error messages depending on error code. As shown, that is completely doable using the error
method of the TestRequest
object.
To learn more, read the official Angular documentation chapter aboutTesting HTTP requests
Testing helpers
If you have helper functions, testing them is basically the same as in any other application in any framework (even Vanilla JS). You do not need TestBed. You just need good old Jasmine, and you test your helpers as pure functions. If your helpers are not pure functions, you should really make them pure.
Coverage
Make sure to configure either Karma (Angular's default test runner), or Jest to give more sensible coverage reports.
By default, if some file is not imported anywhere in the testing scope (no .spec.ts file that has unit test for that specific file or it is not somehow transitively included in another .spec.ts file), it will not be taken into account when calculating test coverage.
This means that there could be pieces of code that the coverage reporter is completely unaware of and can result in reported coverage that is not realistic at all.
You can view examples of both naive and more sensible configurations at:
- Karma with Jasmine - js-karma-jasmine-coverage-examples, using karma-sabarivka-reporter and specific configuration in karma.conf.js
- Jest - js-jest-coverage-examples, using specific configuration in jest.config.js
On the other hand, you will likely want to ignore any files that are either tests themselves, somehow related to tests such as mocks, test related functions or are not relevant for the application itself such as Storybook files.