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 making HTTP requests. 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. I have written an article how to switch to Jest.

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.

Further read

Senior Software Engineer @LeanIX. Co-founder of Sedeo. Passion for software engineering and startups. Looking forward to build great things. 有難うございます。🚀

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store