Skip to main content

Angular Code Coverage

Introduction

Code coverage is a crucial metric in software testing that measures how much of your code is being tested by your automated tests. In Angular applications, code coverage helps you identify which parts of your application are well-tested and which areas need more attention.

By the end of this tutorial, you'll understand what code coverage is, how to enable it in your Angular projects, how to interpret coverage reports, and how to improve your overall test coverage.

What is Code Coverage?

Code coverage is a measurement of how many lines, statements, branches, and functions in your code are executed when your test suite runs. It's typically expressed as a percentage:

  • Statement coverage: Percentage of code statements that have been executed
  • Branch coverage: Percentage of code branches (like if/else statements) that have been executed
  • Function coverage: Percentage of functions that have been called
  • Line coverage: Percentage of executable lines that have been executed

Higher coverage percentages generally indicate more thorough testing, though 100% coverage doesn't necessarily mean your code is bug-free or well-tested.

Setting Up Code Coverage in Angular

Angular CLI projects come with built-in support for code coverage via Karma and Istanbul. Here's how to enable and configure it:

Running Tests with Coverage

To run your tests with coverage reporting, simply add the --code-coverage flag to the test command:

bash
ng test --code-coverage

This will generate a coverage report in your project's /coverage directory.

Configuring Coverage in angular.json

You can also configure code coverage settings in your angular.json file:

json
"test": {
"options": {
"codeCoverage": true,
"codeCoverageExclude": [
"src/polyfills.ts",
"src/testing/**/*"
]
}
}

This configuration:

  1. Enables code coverage by default when running tests
  2. Excludes specific files or directories from coverage calculation

Setting Coverage Thresholds

You can enforce minimum coverage requirements in your karma.conf.js file:

javascript
module.exports = function (config) {
config.set({
// existing config...

coverageReporter: {
dir: require('path').join(__dirname, './coverage'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
],
check: {
global: {
statements: 80,
branches: 70,
functions: 80,
lines: 80
}
}
}
});
};

With this configuration, your tests will fail if coverage falls below the specified thresholds.

Understanding Coverage Reports

After running tests with coverage enabled, you'll have access to detailed reports. Let's explore how to interpret them:

HTML Report

The HTML report is the most user-friendly way to view coverage. Open the index.html file in the /coverage directory with your browser:

Coverage Report Example

The report includes:

  • Overview of total coverage percentages
  • File-by-file breakdown
  • Detailed view showing which lines were executed (green) and which were not (red)

Console Summary

The test runner also outputs a summary to the console:

-----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------------|---------|----------|---------|---------|-------------------
All files | 85.71 | 78.26 | 81.25 | 85.71 |
app | 84.62 | 75 | 80 | 84.62 |
app.component.ts | 100 | 100 | 100 | 100 |
user.service.ts | 72.73 | 50 | 66.67 | 72.73 | 25,35,45
-----------------------|---------|----------|---------|---------|-------------------

Real-World Example: Testing a Service

Let's look at a practical example of improving code coverage for an Angular service:

1. The Service

Here's a simple UserService with methods to fetch, create, and delete users:

typescript
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { User } from './user.model';

@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';

constructor(private http: HttpClient) {}

getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl)
.pipe(
catchError(error => {
console.error('Error fetching users', error);
return throwError(() => new Error('Failed to fetch users'));
})
);
}

createUser(user: User): Observable<User> {
return this.http.post<User>(this.apiUrl, user)
.pipe(
catchError(error => {
console.error('Error creating user', error);
return throwError(() => new Error('Failed to create user'));
})
);
}

deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`)
.pipe(
catchError(error => {
console.error('Error deleting user', error);
return throwError(() => new Error('Failed to delete user'));
})
);
}
}

2. Initial Test with Low Coverage

An incomplete test might look like this:

typescript
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { User } from './user.model';

describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => {
httpMock.verify();
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should get users', () => {
const mockUsers: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];

service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});

const req = httpMock.expectOne('https://api.example.com/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});

This test only covers the getUsers() method and the success path. The coverage report would show low coverage.

3. Improved Test with Better Coverage

Let's enhance our tests to improve coverage:

typescript
// user.service.spec.ts - improved version
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { User } from './user.model';

describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
const apiUrl = 'https://api.example.com/users';

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => {
httpMock.verify();
});

it('should be created', () => {
expect(service).toBeTruthy();
});

describe('getUsers', () => {
it('should return users on success', () => {
const mockUsers: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];

service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});

const req = httpMock.expectOne(apiUrl);
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});

it('should handle errors when fetching users fails', () => {
service.getUsers().subscribe({
next: () => fail('Expected an error, not users'),
error: error => {
expect(error.message).toBe('Failed to fetch users');
}
});

const req = httpMock.expectOne(apiUrl);
req.flush('Error fetching users', {
status: 500,
statusText: 'Server Error'
});
});
});

describe('createUser', () => {
it('should create a user successfully', () => {
const mockUser: User = { id: 1, name: 'John' };

service.createUser(mockUser).subscribe(user => {
expect(user).toEqual(mockUser);
});

const req = httpMock.expectOne(apiUrl);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(mockUser);
req.flush(mockUser);
});

it('should handle errors when creating a user fails', () => {
const mockUser: User = { id: 1, name: 'John' };

service.createUser(mockUser).subscribe({
next: () => fail('Expected an error, not a successful response'),
error: error => {
expect(error.message).toBe('Failed to create user');
}
});

const req = httpMock.expectOne(apiUrl);
req.flush('Error creating user', {
status: 400,
statusText: 'Bad Request'
});
});
});

describe('deleteUser', () => {
it('should delete a user successfully', () => {
const userId = 1;

service.deleteUser(userId).subscribe(() => {
// Should complete successfully
expect(true).toBeTrue();
});

const req = httpMock.expectOne(`${apiUrl}/${userId}`);
expect(req.request.method).toBe('DELETE');
req.flush(null);
});

it('should handle errors when deleting a user fails', () => {
const userId = 1;

service.deleteUser(userId).subscribe({
next: () => fail('Expected an error, not a successful response'),
error: error => {
expect(error.message).toBe('Failed to delete user');
}
});

const req = httpMock.expectOne(`${apiUrl}/${userId}`);
req.flush('Error deleting user', {
status: 404,
statusText: 'Not Found'
});
});
});
});

Now our test suite covers:

  • Success and error cases for all methods
  • Verification of HTTP methods and URLs
  • Handling of request bodies and response data

This comprehensive approach should result in much higher code coverage, potentially 100% for this service.

Best Practices for Angular Code Coverage

Here are some guidelines to help you make the most of code coverage in your Angular projects:

  1. Aim for realistic targets: Instead of blindly targeting 100% coverage, focus on covering critical parts of your application thoroughly.

  2. Test both success and error paths: As demonstrated in our example, test both expected behaviors and error handling.

  3. Focus on logic, not boilerplate: Prioritize testing complex business logic over simple getters/setters or auto-generated code.

  4. Use coverage to identify gaps: Use coverage reports to find untested parts of your application, not just to achieve a certain percentage.

  5. Integrate with CI/CD: Add coverage checks to your continuous integration process to prevent coverage from decreasing over time.

  6. Exclude appropriate files: Some files don't need testing (e.g., environment configurations). Exclude these from coverage calculations.

Beyond Code Coverage: Additional Testing Considerations

While code coverage is valuable, it's not the only measure of test quality:

  1. Edge cases: Coverage doesn't tell you if you're testing edge cases and boundary conditions.

  2. Meaningful assertions: High coverage with weak assertions doesn't provide good test quality.

  3. Integration testing: Unit test coverage doesn't replace the need for integration and end-to-end tests.

  4. Mutation testing: Consider using tools like Stryker to assess how effective your tests are at catching bugs.

Summary

Code coverage is an essential tool in Angular testing that helps you measure and improve the thoroughness of your test suite. In this tutorial, you've learned:

  • What code coverage is and why it matters
  • How to enable and configure code coverage in Angular projects
  • How to read and interpret coverage reports
  • Practical techniques for improving coverage through comprehensive testing
  • Best practices for using coverage effectively

Remember that code coverage is a tool to help you write better tests, not an end goal itself. Use it as part of a broader testing strategy that includes different types of tests and focuses on building reliable, maintainable applications.

Additional Resources

Exercises

  1. Run code coverage on an existing Angular project and identify areas with low coverage.
  2. Add tests to improve coverage for a component in your application.
  3. Configure minimum coverage thresholds for your project.
  4. Try adding coverage reporting to your CI/CD pipeline.
  5. Experiment with excluding specific files or patterns from coverage calculation.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)