How to Angular Unit Testing — Components, Pipes, Services, Directives
Let’s explore how we can test things in Angular apps
Angular is a great framework for highly scalable and enterprise ready web applications. One thing Angular does great is separating your code into modules, components, services, pipes and directives. This separation also enables developers to write better unit tests which can be executed and written faster than end-to-end tests.
In this article, I want to show you how to test anything. Whether it is a component, a directive, a service or a pipe, most things can be properly tested using Angular’s inbuilt testing capabilities.
Testing Angular Components
Components are the building blocks of any modern web app. You can test a lot of stuff with Angular’s built-in testing capabilities. You can for example check the presence of elements, click elements, press keys and simulate events in addition to testing functions and variables.
fixture.debugElement.query(By.css('.reset-button'))
.triggerEventHandler('click', {});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('#error-msg'))).toBe(null);
expect(fixture.componentInstance.isValid()).toBe(false)
If you have entry components (dynamically created components), then you can override the TestingModule to include them:
TestBed.configureTestingModule({
declarations: [ MyDynamicComponent ],
}).overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [ MyDynamicComponent ],
}
});
Often, a component includes other components. Sometimes, we want to check these as well as we could want to check whether they work together without issues. However, focusing on one component and ignoring other components used by it allows us to check it in isolation. Besides, the more components and modules you need to include the longer will your test execution times be. If you want to ignore all Angular components in a spec, you need to include this special schema:
beforeEach(() => TestBed.configureTestingModule({
declarations: [MyComponent],
imports: [],
providers: [MyService],
schemas: [NO_ERRORS_SCHEMA]
}));
Testing Angular Pipes: using mock classes
A Pipe is supposed to transform an incoming value and can be used in both template and TypeScript. Since a pipe is at its core a plain class, we can instantiate it and test if the transformation works.
expect(new MyPipe().transform(0.02)).toEqual('2.00%');
Often, we use pipes in templates. You need to include the pipe when configuring your TestingModule if you are using it in a component’s template. Here’s how you can mock a pipe. Note that the name of the pipe has to be the same as the pipe you are mocking.
@Pipe({ name: 'myPipe' })
class MyPipeMock implements PipeTransform {
transform(param) {
console.log('mocking');
return 'myValue';
}
}
Alternatively, you can use the real pipe and mock the return value of the transform
function by spying on the execution of the function.
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SomeComponentUsingPipe, PipeIWantToMock],
});
spyOn(PipeIWantToMock.prototype, 'transform').and.returnValue('myValue');
});
Testing Angular Services
Services are usually used for performing side effects like making HTTP requests to fetch data from a backend. If a service is not injectable, then we can often instantiate it directly and test it since it is just a plain class.
expect(new MyService().add(2, 3)).toEqual(5);
However, it will be more difficult to mock services which depend on other services (e.g. HttpClient) because you’d need to provide lots of services. An easier way is to mock such a service. Here’s a reusable and configurable way how a service can be mocked:
import { MyInjectableService } from 'services/my-injectable.service';
let config: MockMyInjectableServiceConfig = {};
export interface MockMyServiceConfig {
featureEnabled?: boolean;
}
export class MockMyInjectableService {
public isFeatureEnabled() {
return config.featureEnabled;
}
}
export function createMyInjectableServiceMocker(initialConfig: MockMyInjectableServiceConfig) {
config = initialConfig;
return {
getProvider: () => ({
provide: MyInjectableService,
useFactory: () => new MockMyInjectableService()
}),
setConfig: (c: MockMyInjectableServiceConfig) => config = c
};
}
Perhaps you need to include a service but it is not used directly in your spec? In that case, you can mock the service more easily:
{
provide: MyInjectableService,
useValue: {}
}
I have written a post how you can use mocking to write better tests:
Testing Angular Directives
A Structural directive shapes or reshapes the DOM’s structure, typically by adding, removing, or manipulating elements while an Attribute directive changes the appearance or behavior of a DOM element. Commonly used directives are NgIf and NgFor. We need a component to use these directives in order to test the directive. You could test a directive by testing a component which uses this directive. However, this way you have more unnecessary code inside your spec which you do not want to test. Instead, you can create a very basic component which uses the directive and check the directive in your spec by including the component in your TestingModule:
@Component({
selector: 'my-test-comp',
template: '<input my-tooltip-directive="A tooltip">'
})
class MyTestComponent {
}
Improving the testing experience in Angular: try Jest
Jest can be a viable alternative to the default Angular testing setup. The setup is fairly easy and the configuration & docs are (in my opinion) way better than the Karma + Jasmine setup which is the default in Angular projects. The CLI is a powerful feature which improves the testing experience a lot.
Improving the quality of your tests: try Angular Testing Library
Angular Testing Library is a great library for writing component and integration tests. It is also available for other frameworks like React and Vue. It provides light utility functions on top of DOM Testing Library in a way that encourages better testing practices.
While the setup for a test written with Testing Library looks somewhat similar to setups found in tests using the Angular Testing API the difference lies in the testing approach. With Testing Library, you are highly encouraged to avoid testing implementation details and rather focus on making your tests give you the confidence for which they are intended.
Conclusion
Thank you for reading this article. As you can see, we can test a lot of stuff no matter the type of class: component, directive, pipe and service. Do you have other tips regarding Angular unit testing? Let me know in the comments.