Our content is free thanks to ag-Grid

ag-Grid is the industry leading JavaScript datagrid

ag-grid.com

Create your Angular unit test spies automagically

Post Editor

Creating a mock for unit testing is both necessary and tedious. Let's try and automate that away.

5 min read
post
Cover photo by Dmitry Ratushny on Unsplash.

Create your Angular unit test spies automagically

Creating a mock for unit testing is both necessary and tedious. Let's try and automate that away.

post
Cover photo by Dmitry Ratushny on Unsplash.
post
Cover photo by Dmitry Ratushny on Unsplash.
5 min read
5 min read

A spy is what javascript testing frameworks call an object that can be used instead of any dependency and can override method calls and provide information on the number of calls, etc.
Links to the docs for jasmine, jest, and mocha.

In this article, we'll start with manually creating a spy and work our way to having a reusable and strong-typed function that does that for us. First for Jasmine and then for Jest. The steps would be:

  • implement a mock service having the shape of the original and allowing control over its behavior and responses using Jasmine APIs
  • design what an automated spy-creator (autoSpy) function would return
  • implement the autoSpy
  • augment implementation to use conditional types for working around properties
  • give alternative implementation for Jest users
  • finally, give a Schematic for generating it

The final result on Stackblitz or refer to the SCuri docs on GitHub.

Manually create a spy

Let's take an example of a service being a dependency of our component. We would like to create a spy, a mock of the service if you will, that we'll use in the service's stead. Presenting the AuthorService, that can fetch or update authors:

class AuthorService {
  getAuthor(id: string): Author {
    // .. implementation not important
  }

  updateAuthor(author: Author): 'success' | 'error' {
    // .. implementation not important
  }
}

And our AuthorComponent

export class AuthorComponent {
  author: Author;

  constructor(private s: AuthorService) {}

  ngOnInit() {
    this.author = this.s.getAuthor('1');
  }
}

We would manually create the mock like this:

class AuthorServiceMock implements AuthorService {
  mockGetAuthorResponse: Author;
  mockUpdateAuthorResponse: 'success' | 'error';
  getAuthor() {
    return this.mockGetAuthorResponse;
  }
  updateAuthor() {
    return this.mockUpdateAuthorResponse;
  }
}

describe('AuthorComponent', () => {
  it('should display the author when found by id', () => {
    const service = new AuthorServiceMock();
    service.mockGetAuthorResponse = { name: 'test' } as Author;
    const c = new AuthorComponent(service);
    c.ngOnInit();
    expect(c.author).toEqual({ name: 'test' });
  });
});

Ok, this approach works - live code on Stackblitz. An issue would arise if we change AuthorService. Then we'd need to update the mock too.

EMBEDED - https://stackblitz.com/edit/manual-spy?embed=1&file=src/author.spec.ts&hideExplorer=1&view=editor

Design the autoSpy

What we actually want is to focus on the methods we need for the test case and let the rest be. How would that look like?

describe('AuthorComponent', () => {
  it('should do display the author when found by id', () => {
    const service = autoSpy(ServiceMock);
    service.getAuthor.mockReturn({ name: 'me' } as Author);
    const c = new AuthorComponent(service);
    c.ngOnInit();
    expect(c.author).toEqual({ name: 'me' });
  });
});

Notice there's no need for the AuthorServiceMock. Less code, less maintenance chores!
Ok, we've designed our autoSpy, now for the actual implementation.

Implementation of autoSpy

  1. The function would need to take a constructor (i.e. a new-able something resulting in a thing) and return the object of the constructor type (i.e. something looking like the new-ed up thing)

    function autoSpy(o: new (...args: any[]) => T): T {
      return {} as T;
    }
    

    The type says that the autoSpy accepts constructors i.e. the thing that when called with new will return an instance of type T.

  2. Next, we'd like to strong type that the methods on the returned object are spies so we can spy on calls as well as mock methods

    // prettier-ignore
    function autoSpy(o: new (...args: any[]) => T): T &
      {
        [k in keyof T]: jasmine.Spy;
      }
    {
      return {} as T;
    }
    

    Now the return type is T and something more. For Typescript this means the original shape of the object T is still there, and we augment it with

    {
        [k in keyof T]: T[k] & jasmine.Spy;
    }
    

    This translates to: an object with the same property names (keys) as the type T <=> [k in keyof T] and each of them has the return type of the combination of the original return type T[k] plus jasmine.Spy.

    Let's do an example to make it clearer. For our AuthorService the return type of autoSpy(AuthorService) would be

    type returned = {
      getAuthor(id: string): Author & jasmine.Spy;
      updateAuthor(a: Author): ('success' | 'error') & jasmine.Spy;
    };
    

    We could extract the type to make the function signature readable:

    export type SpyOf<T> = T &
      {
        [k in keyof T]: jasmine.Spy;
      };
    
    export function autoSpy<T>(obj: new (...args: any[]) => T): SpyOf<T> {
      //..
    }
    
  3. Finally, we need to actually implement the promised result

    // SpyOf<T> represents the complex type described in 2.
    
    export function autoSpy<T>(obj: new (...args: any[]) => T): SpyOf<T> {
      const res: SpyOf<T> = {} as any;
    
      const keys = Object.getOwnPropertyNames(obj.prototype);
      keys.forEach((key) => {
        res[key] = jasmine.createSpy(key);
      });
    
      return res;
    }
    

    Read the prototype properties, it's where the methods get attached to in javascript, and for each method instantiate a jasmine Spy!

Properties

There is one small issue here and that is if the dependency we are mocking has properties autoSpy would slap & jasmine.Spy on them too, which would later make it harder to assign values to. So if a property has a type string it would now be of type string & jasmine.Spy. That makes for an error when assigning a string value to it:

1-property

See live code on Stackblitz

To fix that we'll use the conditional types that TS 2.8 introduces.

Originally we had:

export type SpyOf<T> = T &
  {
    [k in keyof T]: jasmine.Spy;
  };

With conditional types:

export type SpyOf<T> = T &
  {
    [k in keyof T]: T[k] extends Function ? jasmine.Spy : never;
  };

If the type of the property is a function then add jasmine.Spy to its type signature, otherwise add the same type so (name: "string" & "string") which amounts to "leave it as is". And we get no error

2-property-conditional-types

See live code on Stackblitz

Jest support

The type would change a bit with Jest:

type SpyOf<T> = T &
  {
    // changes                                  ? ? ?         ? ? ?
    [k in keyof T]: T[k] extends (...args: any[]) => infer R ? jest.Mock<R> : T[k];
  };

export function autoSpy<T>(obj: new (...args: any[]) => T): SpyOf<T> {
  const res: SpyOf<T> = {} as any;

  const keys = Object.getOwnPropertyNames(obj.prototype);
  keys.forEach((key) => {
    // change ? ? ?
    res[key] = jest.fn(key);
  });

  return res;
}

Which adds the strong typing of the result of the method call:

describe('AuthorComponent', () => {
  it('should do display the author when found by id', () => {
    const service = autoSpy(ServiceMock);
    // typescript will spot an error because 'namee' ? is not part of Author interface
    service.mockGetAuthorResponse.mockReturn({ namee: 'me' });
    const c = new AuthorComponent(service);
    c.ngOnInit();
    expect(c.author).toEqual({ namee: 'me' });
  });
});

3-jest-type

Schematic

For generating the autoSpy function using a schematic refer to the scuri:autospy documentation on install and use here.

Summary

We started with a manual mock implementation in Jasmine Spy terms and worked our way to a function that would create the object mocks for us. Called that function autoSpy. Added support for both methods and properties. Finally, added support for Jest Spy too.

Thanks for reading

This autoSpy can be used as part of my SCuri project. Head over to GitHub to check it out.

Discuss with community

Share

About the author

author_image
Georgi Parlakov

Author of Angular libs like SCuri (Angular unit test automation!) and ngx-forms-typed (type your Angular forms!) that make developer's work easier. https://gparlakov.github.io/

author_image

About the author

Georgi Parlakov

Author of Angular libs like SCuri (Angular unit test automation!) and ngx-forms-typed (type your Angular forms!) that make developer's work easier. https://gparlakov.github.io/

About the author

author_image
Georgi Parlakov

Author of Angular libs like SCuri (Angular unit test automation!) and ngx-forms-typed (type your Angular forms!) that make developer's work easier. https://gparlakov.github.io/

NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

RxJSpost
21 January 20214 min read
RxJS in Angular: Part III

In my previous two articles we have discussed how to change our components which solve problems in imperative ways to do that in functional, reactive, RxJS way, and we of course had a lot of fun doing that.

RxJSpost
21 January 20214 min read
RxJS in Angular: Part III

In my previous two articles we have discussed how to change our components which solve problems in imperative ways to do that in functional, reactive, RxJS way, and we of course had a lot of fun doing that.

Read more
RxJSpostRxJS in Angular: Part III

21 January 2021

4 min read

In my previous two articles we have discussed how to change our components which solve problems in imperative ways to do that in functional, reactive, RxJS way, and we of course had a lot of fun doing that.

Read more
Angularpost
20 January 20216 min read
Angular and SOLID principles

In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This is where SOLID principles come into play.

Angularpost
20 January 20216 min read
Angular and SOLID principles

In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This is where SOLID principles come into play.

Read more
AngularpostAngular and SOLID principles

20 January 2021

6 min read

In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This is where SOLID principles come into play.

Read more
Angularpost
14 January 20216 min read
Demystifying Taiga UI root component: portals pattern in Angular

Just before new year we announced our new Angular UI kit library Taiga UI. If you go through Getting started steps, you will see that you need to wrap your app with the tui-root component. Let's see what it does and explore what portals are and how and why we use them.