01 May 2020
5 min

Create your Angular unit test spies automagically

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.

Frequently asked questions

How do I mock an Angular service in unit tests without writing a manual mock class?

You can generate a typed spy object with an autoSpy helper that takes the service class and returns an object whose methods are Jasmine spies. It collects method names from Object.getOwnPropertyNames(obj.prototype) and calls jasmine.createSpy() for each. Your test then configures only what it needs, such as service.getAuthor.mockReturn(...), so changes to the service don't force mock updates.

What is a spy in JavaScript unit testing?

A spy is an object that stands in for a dependency during a test, overriding method calls and recording information such as how many times each method was called. In Jasmine you create one with jasmine.createSpy(), and the same idea applies in Jest with jest.fn(). Spies let a component under test receive controlled responses from a service instead of the real implementation, which keeps the test isolated.

Why do I get a type error when assigning a string to a property of an autoSpy mock?

A naive SpyOf<T> intersects every member with jasmine.Spy, so a property typed as string becomes string & jasmine.Spy and rejects plain values. The fix is a conditional type that adds jasmine.Spy only where T[k] extends Function, leaving data properties with their original type. This lets you assign ordinary values to fields while methods still behave as spies.

How do I use autoSpy with Jest instead of Jasmine?

Replace jasmine.createSpy() with jest.fn() in the helper and change the mapped type so function members become jest.Mock<R>. Use T[k] extends (...args: any[]) => infer R ? jest.Mock<R> : T[k] to infer each method's return type, which gives you strongly typed mock return values. Call sites keep the same autoSpy(AuthorService) shape.

Is there a schematic that generates the autoSpy helper for my Angular project?

The SCuri project provides a schematic, scuri:autospy, that generates the autoSpy function for you. Its documentation on GitHub covers installation and usage. The autoSpy helper described in the article is part of SCuri, so you can add it to a project instead of copying the implementation by hand.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.