How to Achieve 100% Code Coverage with Jest Tests in Angular
Achieving 100% code coverage with Jest unit tests in Angular is important for several reasons. First and foremost, unit tests provide a way to ensure that the codebase is working as intended, by testing the functionality and behavior of individual components, services, and pipes in isolation. This is especially important in large and complex codebases, where it can be difficult to predict how changes to one part of the codebase will affect other parts.
In addition, unit tests help to catch bugs and errors early in the development process, before they have a chance to propagate and become more difficult to fix. This can save time and resources in the long run, by reducing the need for manual testing and debugging.
Furthermore, unit tests also serve as documentation for the codebase, providing a clear and concise way to understand how the code is intended to work. This can be especially helpful for new team members or for developers who are less familiar with the codebase.
Lastly, achieving 100% code coverage with Jest unit tests in Angular provides a way to measure the quality of the codebase, by identifying areas that may be missing tests or that may be more prone to errors. This can help to identify areas that need improvement and guide the development process toward a more robust and reliable codebase.
Achieve 100% Code Coverage
In order to achieve 100% test coverage for an Angular component using Jest, you should test all of the component’s functionality and behavior. This includes testing the component’s inputs, outputs, methods, and any other behavior that is specific to your component.
Here are some things that you should consider testing in your Angular component’s Jest tests:
- Inputs: Test that the component correctly receives and processes inputs passed to it.
- Outputs: Test that the component correctly emits outputs when certain events occur.
- Methods: Test that the component’s methods work as expected and return the expected results.
- DOM: Test that the component’s template is correctly rendered and that the component’s behavior is correctly reflected in the DOM.
- Interaction with Services: Test that the component correctly interacts with any services it depends on.
- Error handling: Test that the component handles errors correctly.
- Lifecycle hooks: Test that the component’s lifecycle hooks are being called correctly.
- State: Test that the component’s state is being correctly managed and updated
- Style: Test that the component’s styles are being applied correctly.
It’s also important to test the component’s interactions with other components and modules and how the component handles edge cases and unexpected inputs.
It’s worth noting that 100% test coverage does not guarantee the absence of bugs in the code, but it does ensure that the codebase is well-tested and that the tests have covered the majority of the codebase.
In addition, it’s important to keep in mind that testing is a balance between thoroughness and maintainability and that it’s important to choose the most meaningful test cases to keep the codebase maintainable.
Example of how you could test Inputs in an Angular component using Jest:
In this test, first, we import the necessary dependencies and create an instance of the component MyComponent
.
Then, we grab the DOM element of the input by calling the querySelector
method of the fixture.nativeElement
which is the root DOM element of the component, this way we can simulate the interaction with the input element.
After that, we set the input value and dispatch a DOM event input
which is the event that is emitted when the value of an <input>
or <textarea>
element is changed.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
});
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
it('should update the name property when the input is changed', () => {
// Get the input element
const input = fixture.nativeElement.querySelector('input');
// Set the input value
input.value = 'test name';
input.dispatchEvent(new Event('input'));
// Trigger change detection
fixture.detectChanges();
// Check that the component's name property was updated
expect(component.name).toEqual('test name');
});
});
Then, we then trigger change detection by calling fixture.detectChanges()
. This is necessary because Angular does not automatically detect changes made directly to the DOM, so we need to manually trigger change detection for the component to update its properties.
Finally, we check that the component’s name
property has been updated with the correct value by using the toEqual
matcher provided by Jest.
It’s important to note that in this test, we are only testing the input handling logic of the component, but a complete test should cover the input handling logic of the component and also the validation of the inputs.
Also, it’s important to test the behavior when the input is invalid or empty, so we can be sure that the component will handle those situations correctly.
Example of how you could test Outputs in an Angular component using Jest:
In this example, the MyComponent
has an output property titleEmitter
which emits the title whenever a button is clicked. The test subscribes to this output property and saves the emitted value in the emittedTitle
variable. Then, the test sets the title
property of the component, simulates a click on the button, and checks that the output emitted the correct value, in this case, the title.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should emit the title when the button is clicked', () => {
const title = 'Test Title';
let emittedTitle: string;
// subscribe to the output and save the emitted value
component.titleEmitter.subscribe(t => emittedTitle = t);
// set the title and click the button
component.title = title;
fixture.debugElement.query(By.css('button')).nativeElement.click();
// check that the output emitted the correct value
expect(emittedTitle).toBe(title);
});
});
It’s worth noting that you need to import the By
class to be able to select the button from the DOM and simulate the click event.
Example of how you could test a method in an Angular component using Jest:
Before giving an example of a unit test of a method in Angular I would like to explain the difference between a method and a function.
In Angular, the difference between a function and a method is a matter of context and usage.
A function is a block of code that can be reused throughout a program. It takes some input (referred to as parameters), performs some computation, and returns an output (referred to as a return value). In TypeScript, a function is defined using the function
keyword, like this:
function add(x: number, y: number): number {
return x + y;
}
A method, on the other hand, is a function that is associated with an object or class. It has access to the internal state of the object or class, and can modify it if needed. In TypeScript, a method is defined inside a class, like this:
class MyComponent {
title: string;
setTitle(newTitle: string) {
this.title = newTitle;
}
}
In Angular, the main difference between a function and a method is that methods are typically used to define the behavior of a component, and functions are used for other purposes, such as utility functions, or services.
In summary, a function is a standalone block of code that performs a specific task and returns a value, while a method is a function that is associated with an object or class and can modify the internal state of that object or class.
Here’s an example of how you could test a method in an Angular component using Jest:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should return the title in uppercase', () => {
const title = 'Test Title';
component.title = title;
const result = component.getTitleInUpperCase();
expect(result).toBe(title.toUpperCase());
});
});
In this example, the MyComponent
has a method getTitleInUpperCase()
that returns the title in uppercase. The test sets the title
property of the component, calls the getTitleInUpperCase()
method and checks that the method returns the correct value, which is the title in uppercase.
It’s worth noting that you could also check the value of the title after the method call to see if it’s updated or not.
Example of how you could test the DOM of an Angular component using Jest:
In this example, the MyComponent
is expected to display the title in a h1
element in the DOM. The test sets the title
property of the component, triggers the change detection mechanism by calling fixture.detectChanges()
and then it select the h1
element in the DOM using the Angular testing utility query()
method and the By.css()
method. Then it compares the textContent of the element with the title.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { By } from '@angular/platform-browser';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display the title in a h1 element', () => {
const title = 'Test Title';
component.title = title;
fixture.detectChanges();
const h1Element = fixture.debugElement.query(By.css('h1')).nativeElement;
expect(h1Element.textContent).toBe(title);
});
});
It’s worth noting that, in order to select the element in the DOM using the By.css()
method you need to import the By
class from @angular/platform-browser
In this way you can test the behavior of the component on the DOM and check if the component is rendering the expected values and elements.
Example of how you could test interactions and services in an Angular component using Jest:
In this example, the MyComponent
has a button that, when clicked, calls a method on the MyService
service. The test uses the spyOn()
method to spy on the getData()
method of the service, and the and.returnValue()
method to return a mock value when the method is called. It then triggers a click on the button and verifies that the service's method was called, and that the component's data
property was set to the expected value.
It’s worth noting that you need to import of operator to use it as a mock return value, Also, you need to import MyService
from the service file.
You can use this approach to test the interactions between a component and a service and verify that the component is using the service correctly and that the service is returning the expected data. Additionally, you can also use this approach to test other interactions, such as event listeners, form submissions, and http requests.
import { TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { MyService } from './my.service';
import { of } from 'rxjs';
describe('MyComponent', () => {
let component: MyComponent;
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MyService],
declarations: [MyComponent]
});
service = TestBed.inject(MyService);
component = TestBed.createComponent(MyComponent).componentInstance;
});
it('should call the service when the button is clicked', () => {
// Spy on the service's method
spyOn(service, 'getData').and.returnValue(of([]));
// Trigger the button click
const button = component.nativeElement.querySelector('button');
button.dispatchEvent(new Event('click'));
// Check that the service's method was called
expect(service.getData).toHaveBeenCalled();
});
it('should set the data property when the service returns data', () => {
// Spy on the service's method
const testData = [1, 2, 3];
spyOn(service, 'getData').and.returnValue(of(testData));
// Trigger the button click
const button = component.nativeElement.querySelector('button');
button.dispatchEvent(new Event('click'));
// Check that the component's data property was set
expect(component.data).toEqual(testData);
});
});
It’s also important to note that when you are testing the component you need to access the nativeElement
property of the component instance to select elements in the DOM.
In summary, testing interactions and services in an Angular component using Jest can be done by creating a spy on the service’s method, triggering the interaction that calls the service’s method, and then verifying that the service’s method was called and that the component’s properties or states were updated correctly.
Example of how you could test an HTTP POST request in an Angular component using Jest:
This test is checking that when the onSubmit()
method of the MyComponent
is called, the component's form values are sent in a POST request to the correct URL using the MyService
service.
First, the test sets up the necessary imports and providers for the test by importing HttpClientTestingModule
and HttpTestingController
from @angular/common/http/testing
, TestBed
from @angular/core/testing
, and the MyComponent
and MyService
from their respective files.
Then, in the beforeEach
block, we configure the TestBed
with the necessary imports and providers, and we create instances of the MyComponent
, MyService
and HttpTestingController
.
In the test block, it starts by spying on the service’s method submitForm
using the spyOn
method, and call the callThrough
method to make the original function call. Then it sets the form's values using the setValue
method of the controls, and triggers the form submission by calling the onSubmit()
method.
Then it checks that the service’s method was called using toHaveBeenCalled()
method of expect
.
Then it uses HttpTestingController
to expect one post request to be sent to the specific URL, and check the request's method and body to be as expected.
Finally, it’s important to note that this test is assuming that the MyService
is correctly configured to make the HTTP request, and that the MyComponent
is correctly using the MyService
to submit the form.
You can also check the response of the request by using req.flush(mockResponse)
method in the test block, where mockResponse
is the response object you want to use.
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { MyService } from './my.service';
describe('MyComponent', () => {
let component: MyComponent;
let service: MyService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [MyService],
declarations: [MyComponent]
});
service = TestBed.inject(MyService);
httpMock = TestBed.inject(HttpTestingController);
component = TestBed.createComponent(MyComponent).componentInstance;
});
it('should make a POST request when the form is submitted', () => {
// Spy on the service's method
spyOn(service, 'submitForm').and.callThrough();
// Set the form values
component.form.controls.name.setValue('test name');
component.form.controls.email.setValue('test@email.com');
// Trigger the form submission
component.onSubmit();
// Check that the service's method was called
expect(service.submitForm).toHaveBeenCalled();
// Check that the HTTP request was made with the correct method and URL
const req = httpMock.expectOne(`https://myapi.com/submit`);
expect(req.request.method).toEqual('POST');
// Check that the HTTP request had the correct body
expect(req.request.body).toEqual({
name: 'test name',
email: 'test@email.com'
});
// Send a response
req.flush({ message: 'Form submitted successfully' });
// Check that the component's message property was updated
expect(component.message).toEqual('Form submitted successfully');
// Verify that there are no outstanding requests
httpMock.verify();
});
});
After we flush the response of the request, we can check that the component’s message property has been updated with the correct message. Finally, we use httpMock.verify()
to ensure that there are no outstanding requests. This will cause the test to fail if there are any requests that have not been handled by expectOne
or expectNone
.
It is important to note that this test is a simplified example and you should test additional cases like error handling and testing the http status code
Example of how you could test error handling in an Angular component using Jest:
First, we use Jest’s jest.spyOn
method to spy on the submitForm
method of the MyService
that the component uses to submit the form. This allows us to check that the method is being called when the form is submitted.
Then, we set the values of the form controls using setValue
method of the form controls to simulate the form submission.
After that, we trigger the form submission by calling the onSubmit
method of the component.
Next, we check that the submitForm
method of the service was called by using the toHaveBeenCalled
matcher.
Then, we use the HttpTestingController
to get the request that was sent when the form was submitted and we flush an error response for that request by passing an error object as the first argument of the flush method. We also set the status code and status text of the error.
Finally, we check that the component’s error property has been updated with the correct error message and call httpMock.verify()
to ensure that there are no outstanding requests.
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { MyService } from './my.service';
describe('MyComponent', () => {
let component: MyComponent;
let service: MyService;
let httpMock: HttpTestingController;
let fixture: ComponentFixture<MyComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [MyService],
declarations: [MyComponent]
});
service = TestBed.inject(MyService);
httpMock = TestBed.inject(HttpTestingController);
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
afterEach(() => {
httpMock.verify();
});
it('should handle error when the form is submitted', () => {
// Spy on the service's method
jest.spyOn(service, 'submitForm').mockImplementation(() => {});
// Set the form values
component.form.controls.name.setValue('test name');
component.form.controls.email.setValue('test@email.com');
// Trigger the form submission
component.onSubmit();
// Check that the service's method was called
expect(service.submitForm).toHaveBeenCalled();
// Get the request and send an error response
const req = httpMock.expectOne('https://my-api.com/submit-form');
req.flush({ message: 'Error submitting form' }, { status: 400, statusText: 'Bad Request' });
// Check that the component's error property was updated
expect(component.error).toEqual('Error submitting form');
});
});
It’s important to note that in this test, we are only testing the error handling logic of the component, but a complete test should cover the error handling logic of the service as well.
Example of testing a lifecycle hook with Jest:
In this example, we are testing the ngOnInit
lifecycle hook of the component MyComponent
.
First, we import the necessary dependencies and create an instance of the component.
Then, we use Jest’s spyOn
method to spy on the loadData
method of the component. This allows us to check that the method is being called when the ngOnInit
lifecycle hook is triggered.
After that, we trigger the ngOnInit
lifecycle hook by calling fixture.detectChanges()
. This method triggers change detection and runs all lifecycle hooks, including the ngOnInit
hook.
Finally, we check that the loadData
method was called by using the toHaveBeenCalled
matcher provided by Jest.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
});
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
it('should call the `loadData` method on init', () => {
// Spy on the `loadData` method
spyOn(component, 'loadData');
// Trigger the `ngOnInit` lifecycle hook
fixture.detectChanges();
// Check that the `loadData` method was called
expect(component.loadData).toHaveBeenCalled();
});
});
It’s important to test the lifecycle hooks to ensure that the component is doing what it’s supposed to do at the right time and in the right order, this way we can be sure that the component will work as expected and also make sure that the component will handle errors or unexpected scenarios correctly.
Example of testing a component’s state with Jest:
In this example, we are testing the state of the component MyComponent
.
First, we import the necessary dependencies and create an instance of the component.
Then, we check that the component’s isLoading
property has the correct default value by using the toBe
matcher provided by Jest.
After that, we call the loadData
method of the component and check that the component's isLoading
property has been updated with the correct value.
It’s important to test the state of the component to ensure that the component is maintaining its internal state correctly and that the component’s behavior is consistent with its state. This way we can ensure that the component is working as expected and that any changes to the component’s state will not cause unexpected behavior or errors.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
});
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should have a default value for the `isLoading` property', () => {
// Check that the component's `isLoading` property has the correct value
expect(component.isLoading).toBe(false);
});
it('should update the `isLoading` property when the `loadData` method is called', () => {
// Call the `loadData` method
component.loadData();
// Check that the component's `isLoading` property was updated
expect(component.isLoading).toBe(true);
});
});
It’s also important to test the component’s state after interacting with it, such as calling a method or emitting an event, to ensure that the component’s state is updated correctly in response to these interactions.
It is also good practice to test the component’s state after it has received input, this way we can ensure that the component is updating its internal state correctly based on the input it receives and that it’s not breaking or throwing errors.
In addition, testing the component’s state after it has undergone a change in its lifecycle also is a good practice, since this way we can ensure that the component’s state is updated correctly based on the lifecycle events it undergoes.
Example of testing a component’s styles with Jest:
In this example, we are testing the styles of the component MyComponent
.
First, we import the necessary dependencies and create an instance of the component.
Then, we check that the component’s element has the correct default styles by using the getComputedStyle
method and the toBe
matcher provided by Jest.
After that, we set the component’s isLoading
property to true, and check that the component's element has the correct updated styles.
Testing styles is important because it ensures that the component is being rendered correctly and that it’s consistent with the intended design.
Also, it’s a good practice to test the component’s styles after receiving input or interacting with it, this way we can ensure that the component’s styles are updated correctly based on the input it receives or the interactions it undergoes.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
});
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should have the correct default styles', () => {
// Get the component's element
const element = fixture.nativeElement;
// Check that the component's element has the correct default styles
expect(getComputedStyle(element).backgroundColor).toBe('white');
expect(getComputedStyle(element).color).toBe('black');
});
it('should update styles when the `isLoading` property changes', () => {
// Set the component's `isLoading` property
component.isLoading = true;
fixture.detectChanges();
// Get the component's element
const element = fixture.nativeElement;
// Check that the component's element has the correct updated styles
expect(getComputedStyle(element).backgroundColor).toBe('grey');
expect(getComputedStyle(element).color).toBe('white');
});
});
It’s important to note that Jest does not provide built-in support for interacting with the DOM, so you might need to use additional libraries such as jest-dom, jest-styled-components, or jest-raw-loader to work with the styles of the component.
How to Achieve 100% Code Coverage with Jest Unit Tests in Angular checklist!
By following these steps, you should be able to write Jest tests that cover 100% of your Angular code.
- Create a test file for the component or service you want to test. The file should have the same name as the component or service, with the suffix “.spec.ts”.
- Import the component or service you want to test, as well as any dependencies it has.
- Use the
describe()
andit()
functions to create test blocks.describe()
is used to group related tests together, whileit()
is used to define individual test cases. - Use the
beforeEach()
function to set up any necessary conditions for your tests. This can include creating instances of the component or service, as well as any mock data or services. - Use the
expect()
function to make assertions about the component or service's behavior. This can include checking the component's output, or checking that certain methods have been called. - Use the
debugElement
andby.css
orby.directive
to test the DOM element of the component - Use the
fixture.detectChanges()
method in each test case to trigger change detection on the component - Use
fixture.whenStable()
to ensure that all the async calls are finished. - Run the test with the
jest
command, which will generate a coverage report that shows the percentage of code that is covered by tests. - Make sure that your tests cover all branches and lines.
In conclusion, achieving 100% code coverage with unit tests in Angular using Jest requires a thorough and comprehensive testing strategy. Unit tests should focus on testing the behavior and functionality of individual components, services, and pipes in isolation, rather than the implementation details. By testing the public API of the code and mocking or stubbing out any dependencies, we can ensure that each unit is working as intended and that any changes made to the codebase will not cause unexpected errors or behavior.
Additionally, it’s important to test the component’s state, styles, and lifecycle hooks, as well as any interactions and services that the component may have, to ensure that the component is working as expected. To achieve 100% code coverage, it’s necessary to use code coverage tools that can measure the percentage of the code that is covered by tests. Jest provides built-in support for code coverage, which can be configured in the Jest configuration file. However, it’s important to keep in mind that achieving 100% code coverage is not always possible and that the goal of testing is to ensure that the code is working correctly, not to achieve a specific coverage percentage.