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.

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
-
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 typeT
. -
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 objectT
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 typeT[k]
plusjasmine.Spy
.Let's do an example to make it clearer. For our
AuthorService
the return type ofautoSpy(AuthorService)
would betype 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> { //.. }
-
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:
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
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' });
});
});
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.