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

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
Link to this section

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:

<>Copy
class AuthorService { getAuthor(id: string): Author { // .. implementation not important } updateAuthor(author: Author): 'success' | 'error' { // .. implementation not important } }

And our AuthorComponent

<>Copy
export class AuthorComponent { author: Author; constructor(private s: AuthorService) {} ngOnInit() { this.author = this.s.getAuthor('1'); } }

We would manually create the mock like this:

<>Copy
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
Link to this section

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?

<>Copy
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
Link to this section

  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)

    <>Copy
    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

    <>Copy
    // 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

    <>Copy
    { [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

    <>Copy
    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:

    <>Copy
    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

    <>Copy
    // 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
Link to this section

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-property1-property

See live code on Stackblitz

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

Originally we had:

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

With conditional types:

<>Copy
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-types2-property-conditional-types

See live code on Stackblitz

Jest support
Link to this section

The type would change a bit with Jest:

<>Copy
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:

<>Copy
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-type3-jest-type

Schematic
Link to this section

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

Summary
Link to this section

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
Link to this section

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

Share

About the author

author_image

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

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/

Looking for a JS job?
Job logo
Senior Frontend Software Engineer (Angular)

Argument

Ukraine
Remote
$54k - $72k
Job logo
Front-End Web Software Engineer (Angular12 + ASP.NET)

MWS Technology

Ukraine
Remote
$36k - $60k
Job logo
Angular Software Developer

Salamander Technologies

America
Remote
$80k - $95k
Job logo
Senior Front End Developer - Angular

triValence

United States
Remote
$125k - $160k
More jobs

Featured articles