Fabrizio Fortunato

Snapshot testing Angular applications

July 02, 2018

In one of my previous articles, I’ve explored how we can start unit testing Angular applications with Jest. Now its time to introduce another exciting feature that Jest provides us: Snapshot Testing.

Intro

One of the main advantages of using Jest over Karma + Jasmine is the significant decrease in execution time for the unit tests. What if Jest allows us to decrease, not only, execution time but also the actual writing time of a unit test?

Welcome to snapshot testing.

As the name suggests it, snapshot testing takes any serialisable object, create a snapshot and then compare your changes with the previously taken snapshot. Jest support out of the box snapshot testing an example of a simple snapshot test can be the following:

describe('FlightService', () => {
  it('Should return a list of flights', () => {
    const flights = FlightService.list();
    expect(flights).toMatchSnapshot();
  });
});

Upon test execution, Jest takes your object, serialise it into a string, save the representation into a file like the following:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FlightService Should return a list of flights 1`] = `
Array [
  Object {
    "arrivalDatetime": "2018-06-15T21:00:00",
    "departure": "DUB",
    "departureDatetime": "2018-06-15T17:25:00",
    "destination": "WRO",
    "flightNumber": "FR153",
  },
  Object {
    "arrivalDateteim": "2018-06-16T20:35:00",
    "departure": "DUB",
    "departureDatetime": "2018-06-16T16:30:00",
    "destination": "CIA",
    "flightNumber": "FR154",
  },
  Object {
    "arrivalDatetime": "2018-06-15T20:55:00",
    "departure": "DUB",
    "departureDatetime": "2018-06-18T17:15:00",
    "destination": "MAD",
    "flightNumber": "FR155",
  },
]
`;

Any other execution of the test is compared to the current object snapshot with the previous one stored in the file. The whole functionality boils down to string comparison between the two snapshots. While using snapshot testing, Jest compares the current snapshots with the previous version. That’s why it is necessary to commit the snapshots files along with your test files.

When using snapshot testing, we have to get it right the first time. We are creating the gold standard test, and we have to treat it as the most accurate test possible.

When

In the React ecosystem, snapshot testing is recommended primarily for UI components, where you can quickly render components leveraging jsdom. Snapshot testing can be used alongside unit testing or in some cases also replace it entirely. The decision is related to the status of your project. If your current project doesn’t much testing coverage than snapshot testing can be a way to increase coverage without any pain; on the other hand, if a project has already a high unit test coverage than you can use it as support testing to perform regression testing.

ng-world

Snapshot testing is not only available in the React world. We can use and apply snapshot testing in an Angular application also.

I assume that your application is already configured for using Jest, if not you can check out my previous article.

An Angular application is a collection of components, directives, services and pipes which combined make a module. Angular makes it easy to test any element of your application, and we can apply snapshot testing to each of those elements.

Component

In an Angular component there are few parts that we can unit test:

  • Public methods of a component
  • Template rendering
  • Input/Output

Snapshot testing can cover all those different parts and at the same time speed up your test creation and coverage.

Let’s take the following FlightInfoComponent used to display informations about a selected flight:

import { Input, Component } from '@angular/core';

@Component({
  selector: 'flight-info',
  template: `
<mat-list-item>
  <div class="fare">
    <span class="airport">{{ flight.departure }}</span>
    <small>{{ flight.departureDatetime | date:'short' }}</small>
  </div>
  <div class="fare"> 
    <span class="airport">{{ flight.destination }}</span>
    <small>{{ flight.arrivalDatetime | date:'short' }}</small>
  </div>
</mat-list-item>
  `,
  styleUrls: [ './flight-info.component.css' ]
})
export class FlightInfoComponent  {
  @Input() flight;
}

The FlightInfoComponent is a dumb component, it doesn’t hold any logic, and its only concern is displaying information received as @Input. What is necessary to test here is that the information is displayed correctly, like a pure function, given a specific input we are expecting the same output.

Snapshot testing shines in this scenarios. The component fixture is a serialisable object; therefore, we can snapshot it.

it('Should display a flight', () => {
  const fixture = TestBed.createComponent(FlightInfoComponent);
  expect(fixture).toMatchSnapshot();
});

We can now pass data as @Input for FlightInfoComponent and generate a snapshot that ensures our component UI is predictable.

  it('Should display a flight from DUB to WRO', () => {
    component.flight = {
      flightNumber: 'FR153',
      departure: 'DUB',
      destination: 'WRO',
      departureDatetime: '2018-06-15T17:25:00',
      arrivalDatetime: '2018-06-15T21:00:00'
    };
    fixture.detectChanges();
    expect(fixture).toMatchSnapshot();
  });

The snapshot created will be the following:

exports[`FlightInfoComponent Should display a flight from DUB to WRO 1`] = `
<flight-info
  flight={[Function Object]}
>
  <mat-list-item
    class="mat-list-item"
  >
    <div
      class="mat-list-item-content"
    >
      <div
        class="mat-list-item-ripple mat-ripple"
        mat-ripple=""
        ng-reflect-disabled="true"
        ng-reflect-trigger="[object HTMLUnknownElement]"
      />
      <div
        class="mat-list-text"
      />
      <div
        class="fare"
      >
        <span
          class="airport"
        >
          DUB
        </span>
        <small>
          6/15/18, 5:25 PM
        </small>
      </div>
      <div
        class="fare"
      >
        <span
          class="airport"
        >
          WRO
        </span>
        <small>
          6/15/18, 9:00 PM
        </small>
      </div>
    </div>
  </mat-list-item>
</flight-info>
`;

We can notice that the template of our component is rendered correctly, displaying the information about our flight.

Service/Pipe

After components, we can apply snapshot testing to services and pipes. The concept is the same that we use for components with the exception that we can omit TestBed. Services and pipes are javascript classes and TestBed is only used for handling the dependencies, which can be easily mocked, for unit tests, when invoking the constructor of such classes.

I’m grouping services and pipes because the approach to testing is the same.

Let’s define our FlightListService:

import { Injectable } from '@angular/core';

@Injectable()
export class FlightListService {
  public list() {
    return [
      { flightNumber: 'FR153', departure: 'DUB', destination: 'WRO', departureDatetime: '2018-06-15T17:25:00', arrivalDatetime: '2018-06-15T21:00:00' },
      { flightNumber: 'FR153', departure: 'WRO', destination: 'DUB', departureDatetime: '2018-06-15T21:25:00', arrivalDatetime: '2018-06-15T23:05:00' },
      { flightNumber: 'FR154', departure: 'DUB', destination: 'CIA', departureDatetime: '2018-06-16T16:30:00', arrivalDatetime: '2018-06-16T20:35:00' },
      { flightNumber: 'FR154', departure: 'CIA', destination: 'DUB', departureDatetime: '2018-06-16T21:00:00', arrivalDatetime: '2018-06-16T23:15:00' },
      { flightNumber: 'FR155', departure: 'DUB', destination: 'MAD', departureDatetime: '2018-06-18T17:15:00', arrivalDatetime: '2018-06-15T20:55:00' },
      { flightNumber: 'FR155', departure: 'MAD', destination: 'DUB', departureDatetime: '2018-06-18T21:30:00', arrivalDatetime: '2018-06-15T23:10:00' },
    ]; 
  }

  public single(departure, destination) {
    const flight = this.list()
    .filter(el =>
      (el.departure === departure &&
        el.destination === destination)
    );
    return flight ? flight[0] : undefined;
  }
}

So now start testing one of the public methods available in the service.

import { FlightListService } from './FlightListService';

describe('FlightListService', () => {
  let service;

  beforeEach(() => {  
    service = new FlightListService();
  });

  it('Should return a single fligth', () => {
    expect(service.single('DUB', 'MAD')).toMatchSnapshot();
  })
})

In a real-world application the FlightListService, rather than returning a hardcoded list of flights, will contact an API to get the list of all the available flights.

Our list method can be rewritten as follows:

public listAPI() {
  return this.$http.get(
      'https://murmuring-ocean-10826.herokuapp.com/en/api/2/flights/from/DUB/to/STN/2014-12-02/2015-02-02/250/unique/?limit=15&offset-0'
  );
}

There are two choices now for testing the list method. We can proceed to mock the HTTP call and returned some values during the test. Alternatively, another option is to continue doing the HTTP call and get the value directly from a real service.

Choosing the first option let the test fall into unit tests since we are testing the method in isolation. We can easily mock dependencies by using jest mock functions

describe('FlightListService', () => {
  let service;
  const http = {  
    get: jest.fn()
  };

  beforeEach(() => {
    service = new FlightListService(
      http as any
    );
  });

  it('Should return all the routes from API', () => {
    expect(service.listAPI()).toMatchSnapshot();
  });
});

The previous test, using mocking values, doesn’t add much value to our test suite. We are testing a tautology, given that we are not doing anything with the data returned.

The second option that we have is to use snapshot testing for integration tests. We are testing functionality spanning across multiple units. We cannot mock HttpClient now since we want our test make the actual API call.

import { TestBed } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';

import { FlightListService } from './flight-list.service';

describe('FlightListService Integration tests', () => {
  let service: FlightListService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule],
      providers: [FlightListService]
    });

    service = TestBed.get(FlightListService);
  });

  it('Should return all the routes from API', done => {
    service.listAPI().subscribe(res => {
      expect(res).toMatchSnapshot();
      done();
    });
  });
});

The integration test checks that the API is not changing or breaking the contract of the response, giving our application an extra safety measure especially when you are not in control of the API.

NGRX

If you are developing a medium/big Angular application chances are that you are using NGRX for managing your state. By convention, the top-level state is an object or some other key-value collection like a Map, but technically it can be any type. Still, you should do your best to keep the state serialisable. Don’t put anything inside it that you can’t easily turn into JSON.

The previous is an extract from reduxjs documentation. The state in redux is a serialisable object that we can test using snapshot testing.

Let’s take the common counter reducer example and apply snapshot test to it.

export const counter: ActionReducer<number> = (state: number = 0, action: Action) => {
    switch(action.type){
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
};
import {counter} from "./counter";

describe('The counter reducer', () => {
  it('should return current state when an invalid action is dispatched', () => {
      const actual = counter(0, {type: 'INVALID_ACTION'});
      expect(actual).toMatchSnapshot();
  });

  ...
});

snapshot failed

Testing, in general, improve the quality of our application and let us feel more comfortable while making new changes, that we are not breaking any existing functionality. We write tests because our application changes over time and we want to make sure that we don’t break previous functionalities.

We have explored how to create snapshots for different elements of our Angular application what is missing now is how to update the snapshots if something has changed in our code. There is a command:

npm run jest -- -u

The command updates all the snapshots to the current version of your objects, ignoring and discarding the previous one. Don’t forget to commit once again the updated snapshots.

You can find all the examples used in the article in https://stackblitz.com/edit/angular-snapshot-testing feel free to clone it and start exploring snapshot testing.


Head of Frontend at RyanairLabs @izifortune
Fabrizio Fortunato © 2021, Built with Gatsby