When creating unit tests or integration tests for Angular 8, we often use mocking - such as mocking services.
We sometimes want to fix up the intellisense of our mocks when we create a spy object using Jasmine (of which Angular tests most often are written in - the 'NUnit for Javascript world'). Here is how we can achieve that.
First off, create a new file called Spied.ts and add this Typescript:
export type Spied<T> = {
[Method in keyof T]: jasmine.Spy;
};
A little bit of terminology here for .NET coders concerning Javascript tests:
- Spy object : Mock object
- Using .and.returnValue(of(somedata)) : Equal to using Moq Setup method to return some data for given method
- Expect in Jasmin : Similar to Assert in MSTest and NUnit.
This builds a
mapped type that maps to a jasmine.Spy object, see the explanation of a mapped type here:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#mapped-types
We now can declare our mock objects as a 'Spied' object like this example:
let mockHeroService: Spied<HeroService>
mockHeroService = jasmine.createSpyObj(['getHeroes', 'addHero', 'deleteHero']);
The great thing about this then is that we now have decent Intellisense in place! Look at this video from VsCode as proof!
To handle dependency injection scenarios do like in this example:
import { VoterService, ISession } from "src/app/events";
import { of } from "rxjs";
import { Spied } from "src/app/common/Spied";
import { HttpClient } from "@angular/common/http";
describe('VoterService', () => {
let voterService: VoterService;
let mockHttp: Spied<HttpClient>;
beforeEach(() => {
mockHttp = <Spied<HttpClient>>jasmine.createSpyObj('mockHttp', ['delete', 'post']);
voterService = new VoterService(mockHttp);
console.log('Inside beforeEach');
});
describe('deleteVoter', () => {
it('should remove the voter from the list of voters', () => {
var session = { id: 6, name: "John", voters: ["joe", "john"] };
mockHttp.delete.and.returnValue(of(false));
console.log(voterService);
voterService.deleteVoter(3, <ISession>session, "joe");
expect(session.voters.length).toBe(1);
});
});
});
We can then adjust our constructor to include the '| any' modifier of the injected parameter:
import { Injectable, Inject } from '@angular/core';
import { ISession } from '../shared';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class VoterService {
constructor(@Inject(HttpClient) private http: HttpClient | any) {
}
..
Note that we here adjust the constructor to not only accept the concrete class HttpClient but also 'any' allowing us to inject the mock object. We could alter this and introduce an interface for example instead for a more elegant approach.
In case you get build errors like when running
ng build
stating that 'jasmine' could not be found, try out this:
Inside tsconfig.json, explicitly add 'jasmine' for your 'types' like this:
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"types": [ "jasmine" ],
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
}
And then put the single line on top to import jasmine like this in Spied.ts:
import 'jasmine';