DEV Community

K
K

Posted on

Isolated unit testing and deep integration testing in Angular

Software testing is the process of evaluating the functionality of the application and checking whether the expected results match the actual results of the software. There are a lot of different ways software can be tested, varying from unit testing, integration testing, end-to-end testing, manual testing, accessibility testing, and so on.

This article is taken from the book Angular Projects by Zama Khan Mohammed. This book will be your practical guide when it comes to building optimized web apps using Angular. It explores a number of popular features, including the experimental Ivy rendered, lazy loading, and differential loading, among others. To follow along with the examples implemented in this article, you can download the code from the book’s GitHub repository

In this article, we will be looking at isolated unit testing and deep integration testing in Angular 8. We will take an already existing application built from the Angular CLI, which has no tests written for it, except for the default test files that were generated by the Angular CLI while generating different files. This is a simple application that uses the Behance API to display a collection of design projects.

Isolated unit testing

Isolated unit testing is the process of isolating a class or function and testing it as a regular JavaScript code. In such test cases, we don't worry about the framework-specific stuff. We will be doing isolated unit testing for our ColorNamerPipe class, which looks as follows:

import { Pipe, PipeTransform } from '@angular/core';
import * as colorNamer from 'color-namer';

@Pipe({
    name: 'colorNamer'
})
export class ColorNamerPipe implements PipeTransform {

    transform(value: any, args?: any): any {
        return colorNamer(`rgb(${value.r},${value.g},${value.b})`).html[0].name;
    }
}
Enter fullscreen mode Exit fullscreen mode

We will test this code without worrying about the decorator on the class, that is, ColorNamerPipe. What this pipe takes is an object with r, g, and b values and uses an npm module to return the name of the color.

The Angular CLI already initializes the test files for pipes as isolated unit tests.

In the following code, we can see that Angular has prepared a basic test to check whether the pipe has been created:

import { ColorNamerPipe } from './color-namer.pipe';

describe('ColorNamerPipe', () => {
    it('create an instance', () => {
        const pipe = new ColorNamerPipe();
        expect(pipe).toBeTruthy();
    });
});
Enter fullscreen mode Exit fullscreen mode

Let's write our new test case, which will pass various values and check whether the pipe returns the correct value or not:

...
describe('ColorNamerPipe', () => {
    ...

    it('should return current color', () => {
        const pipe = new ColorNamerPipe();
        let transformedValue = pipe.transform({
            r: 255,
            g: 255,
            b: 255
        });
        expect(transformedValue).toEqual('white');

        transformedValue = pipe.transform({
            r: 255,
            g: 0,
            b: 0,
        });
        expect(transformedValue).toEqual('red');
        transformedValue = pipe.transform({
            r: 0,
            g: 255,
            b: 0,
        });
        expect(transformedValue).toEqual('lime');
   });
});
Enter fullscreen mode Exit fullscreen mode

In the preceding code, we are testing a particular block of code only. This is why it is called an isolated test case—because our test case is isolated only to one single feature. In this particular test case, we are testing whether the transformed value is returning as expected or not.

Deep integration testing

Isolated unit tests are good when we are testing a class without depending on the integration of the framework that we are using. But when it comes to testing a class alongside the framework, then we need to use integration testing. In integration testing, we test the behavior of a class/function as a whole to ensure that it works cohesively with the framework.

In deep integration tests, we let the framework run all the classes, just like it would do when it runs in its environment (such as a browser). We might want to mock some classes and functions, but overall we let the framework do most of the heavy lifting for us.

Angular uses dependency injection to create different Angular components, and it does so by using @NgModule. For testing, Angular provides TestBed, which can be used to create a TestModule that emulates Angular's @NgModule.

Deep integration tests for the component

In this section, we will be performing an integration test on our component by mocking an observable with an expected type of data. This will help us understand whether the data that's passed is flowing properly in our component and that the expected result is being obtained. We have already successfully configured TestModule for the spec of our HomeComponent.

Let's add some data to our HomeComponent and check whether we can find the items that are being displayed by the template:

beforeEach(() => {
    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
    component.posts$ = of([{id: 1, name: 'Title 1', covers: {115: 'image 
    1.jpg'}}, {id: 1, name: 'Title 2', covers: {115: 'image 2.jpg'}}]);
    fixture.detectChanges();
});

Enter fullscreen mode Exit fullscreen mode

Here, in beforeEach, before we call our fixture to detect changes in the component, we've added a couple of posts. Let's add a couple of tests to test whether it has added two cards in the DOM and if the first card's title is like it was in the data we passed:

describe('HomeComponent', () => {
    ...
    it ('show have 2 post cards', () => {
        const cards = fixture.debugElement.queryAll(By.css('.card'));
        expect(cards.length).toEqual(2);
    });

    it ('should have the title in the card', () => {
        const cards = fixture.debugElement.queryAll(By.css('.card'));
        expect(cards[0].query(By.css('.card-
        text')).nativeElement.textContent)
            .toEqual('Title 1');
    });
});
Enter fullscreen mode Exit fullscreen mode

By doing this, we have performed a deep test, which not only verifies whether the element with the card class has been created but also checks whether the title matches in the first element.

Deep integration tests for the service

Our service, BehanceService, uses HttpClient and has a couple of methods, getPosts and getPost, both of which call an HTTP call. In our testing, we don't want to call the HTTP call and test the actual API. Instead, we need to mock the HTTP call. We already included HttpClientTestingModule in the BehanceService spec file, which will help us to mock the API so that the actual HTTP request doesn't go out. Instead, we return a mocked response:

describe('BehanceService', () => {
    ...

    it('should call posts api', () => {
        const response = 'response';
        const service: BehanceService = TestBed.get(BehanceService);
        const httpMock: HttpClientTestingBackend = 
        TestBed.get(HttpTestingController);

        service.getPosts().subscribe(data => {
            expect(data).toEqual(response);
        });

        const req = httpMock.expectOne(
            (request: any) => {
                return request.url === environment.api + 
                'collections/170716829/projects?per_page=20&page=' + 1 + 
                '&api_key=' + environment.token;
            }
        );

        expect(req.request.method).toEqual('JSONP');
        req.flush(response);
    });
});
Enter fullscreen mode Exit fullscreen mode

Here, we have got the HTTP mock from HttpTestingController. We call the getPosts method and expect the data to be returned with a response. Then, we use httpMock as we expect a URL to be called and then use req.flush to execute the call, which should run the expectation inside the subscription.

Let's also go ahead and test the getPost method in a similar fashion:

describe('BehanceService', () => {
    ...

    it('should call post api', () => {
        const id = 'id';
        const response = 'response';

        const service: BehanceService = TestBed.get(BehanceService);
        const httpMock: HttpClientTestingBackend = 
        TestBed.get(HttpTestingController);

        service.getPost(id).subscribe(data => {
            expect(data).toEqual(response);
        });

        const req = httpMock.expectOne(
            (request: any) => {
                return request.url === environment.api + 'projects/' + id + 
                '?api_key=' + environment.token;
            }
        );

        expect(req.request.method).toEqual('JSONP');
        req.flush(response);
    });
});
Enter fullscreen mode Exit fullscreen mode

In these tests, we are following the preceding concept of doing a deep integration test, but on service instead of a component. In this case, we are testing whether the response is equal to the mock response that we created. It also checks the type of request that was made. The deeper the integration test goes, the better, because it does a thorough check to ensure no bugs pass through.

This article walked you through the two different ways of testing your Angular application: isolated unit testing and integrated testing. While isolated unit testing tests a class or function in isolation as regular JavaScript code, deep integration testing lets you test a class alongside a framework.

If you found this post useful, do check out the book, Angular Projects by Packt Publishing. In this book, you will explore Angular 8 and its latest features including Ivy renderer, Lazy loading, and differential loading by building 9 different projects.

Top comments (0)