Mocking Angular 9 Services with Jasmine
I decided it'd be easier to get rid of helper.ts
and mock what's returned by the service. I also changed to import HttpClientTestingModule
so the service can be instantiated, even if its HttpClient
is never used. Here's my search.component.spec.ts
after this refactoring:
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchComponent } from './search.component';
import { MockActivatedRoute } from '../shared/search/mocks/routes';
import { SearchService } from '../shared';
import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('SearchComponent', () => {
let component: SearchComponent;
let fixture: ComponentFixture<SearchComponent>;
let mockActivatedRoute: MockActivatedRoute;
let mockSearchService: SearchService;
beforeEach(async(() => {
mockActivatedRoute = new MockActivatedRoute({term: 'nikola'});
TestBed.configureTestingModule({
declarations: [SearchComponent],
providers: [
{provide: ActivatedRoute, useValue: mockActivatedRoute}
],
imports: [FormsModule, RouterTestingModule, HttpClientTestingModule]
}).compileComponents();
}));
beforeEach(() => {
// mock response
mockSearchService = TestBed.inject(SearchService);
mockSearchService.search = jasmine.createSpy().and.returnValue(of([]));
// initialize component
fixture = TestBed.createComponent(SearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should search when a term is set and search() is called', () => {
component = fixture.debugElement.componentInstance;
component.query = 'J';
component.search();
expect(mockSearchService.search).toHaveBeenCalledWith('J');
});
it('should search automatically when a term is on the URL', () => {
fixture.detectChanges();
expect(mockSearchService.search).toHaveBeenCalledWith('nikola');
});
});
For another test, I did something similar, returning expected data from the service.
import { EditComponent } from './edit.component';
import { TestBed } from '@angular/core/testing';
import { Address, Person, SearchService } from '../shared';
import { MockRouter, MockActivatedRoute } from '../shared/search/mocks/routes';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { of } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('EditComponent', () => {
let mockSearchService: SearchService;
let mockActivatedRoute: MockActivatedRoute;
let mockRouter: MockRouter;
beforeEach(() => {
mockActivatedRoute = new MockActivatedRoute({id: 1});
mockRouter = new MockRouter();
TestBed.configureTestingModule({
declarations: [EditComponent],
providers: [
{provide: ActivatedRoute, useValue: mockActivatedRoute},
{provide: Router, useValue: mockRouter}
],
imports: [FormsModule, HttpClientTestingModule]
}).compileComponents();
mockSearchService = TestBed.inject(SearchService);
});
it('should fetch a single record', () => {
const fixture = TestBed.createComponent(EditComponent);
const person = new Person({id: 1, name: 'Gary Harris'});
person.address = new Address({city: 'Denver'});
// mock response
spyOn(mockSearchService, 'get').and.returnValue(of(person));
// initialize component
fixture.detectChanges();
// verify service was called
expect(mockSearchService.get).toHaveBeenCalledWith(1);
// verify data was set on component when initialized
const editComponent = fixture.debugElement.componentInstance;
expect(editComponent.editAddress.city).toBe('Denver');
// verify HTML renders as expected
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h3').innerHTML).toBe('Gary Harris');
});
});
Related videos on Youtube
Matt Raible
Web developer and Java Champion that loves to architect and build slick-looking UIs using CSS and JavaScript. When he's not evangelizing Okta and open source, he likes to ski with his family, drive his VWs, and enjoy craft beer.
Updated on June 04, 2022Comments
-
Matt Raible almost 2 years
With Angular 7, I'm able to mock my
SearchService
with Jasmine by creating a few classes. The first is ahelper.ts
file that has a class you can extend./// <reference path="../../../../../node_modules/@types/jasmine/index.d.ts"/> export interface GuinessCompatibleSpy extends jasmine.Spy { /** By chaining the spy with and.returnValue, all calls to the function will return a specific * value. */ andReturn(val: any): void; /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied * function. */ andCallFake(fn: Function): GuinessCompatibleSpy; /** removes all recorded calls */ reset(); } export class SpyObject { static stub(object = null, config = null, overrides = null) { if (!(object instanceof SpyObject)) { overrides = config; config = object; object = new SpyObject(); } const m = {}; Object.keys(config).forEach((key) => m[key] = config[key]); Object.keys(overrides).forEach((key) => m[key] = overrides[key]); for (const key in m) { object.spy(key).andReturn(m[key]); } return object; } constructor(type = null) { if (type) { for (const prop in type.prototype) { let m = null; try { m = type.prototype[prop]; } catch (e) { // As we are creating spys for abstract classes, // these classes might have getters that throw when they are accessed. // As we are only auto creating spys for methods, this // should not matter. } if (typeof m === 'function') { this.spy(prop); } } } } spy(name) { if (!this[name]) { this[name] = this._createGuinnessCompatibleSpy(name); } return this[name]; } prop(name, value) { this[name] = value; } /** @internal */ _createGuinnessCompatibleSpy(name): GuinessCompatibleSpy { const newSpy: GuinessCompatibleSpy = <any>jasmine.createSpy(name); newSpy.andCallFake = <any>newSpy.and.callFake; newSpy.andReturn = <any>newSpy.and.returnValue; newSpy.reset = <any>newSpy.calls.reset; // revisit return null here (previously needed for rtts_assert). newSpy.and.returnValue(null); return newSpy; } }
Here is the
search.service.ts
I'm trying to test:@Injectable({ providedIn: 'root' }) export class SearchService { constructor(private http: HttpClient) { } getAll() { return this.http.get('assets/data/people.json'); } search(q: string): Observable<any> { // implementation } get(id: number) { // implementation } save(person: Person) { // implementation } }
And here's my
search.service.mock.ts
:import { SpyObject } from './helper'; import { SearchService } from '../search.service'; import Spy = jasmine.Spy; export class MockSearchService extends SpyObject { getAllSpy: Spy; getByIdSpy: Spy; searchSpy: Spy; saveSpy: Spy; fakeResponse: any; constructor() { super(SearchService); this.fakeResponse = null; this.getAllSpy = this.spy('getAll').andReturn(this); this.getByIdSpy = this.spy('get').andReturn(this); this.searchSpy = this.spy('search').andReturn(this); this.saveSpy = this.spy('save').andReturn(this); } subscribe(callback: any) { callback(this.fakeResponse); } setResponse(json: any): void { this.fakeResponse = json; } }
And then I mock it in a test.
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { SearchComponent } from './search.component'; import { MockSearchService } from '../shared/search/mocks/search.service'; import { MockActivatedRoute, MockRouter } from '../shared/search/mocks/routes'; import { SearchService } from '../shared'; import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { FormsModule } from '@angular/forms'; describe('SearchComponent', () => { let component: SearchComponent; let fixture: ComponentFixture<SearchComponent>; let mockSearchService: MockSearchService; let mockActivatedRoute: MockActivatedRoute; beforeEach(async(() => { mockSearchService = new MockSearchService(); mockActivatedRoute = new MockActivatedRoute({'term': 'peyton'}); TestBed.configureTestingModule({ declarations: [ SearchComponent ], providers: [ {provide: SearchService, useValue: mockSearchService}, {provide: ActivatedRoute, useValue: mockActivatedRoute} ], imports: [FormsModule, RouterTestingModule] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(SearchComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });
This works with Angular 7. However, when I try it with Angular 9, I have to remove the
// <reference path="..."/>
at the top ofhelper.ts
to fix some compiler errors.ERROR in node_modules/@types/jasmine/index.d.ts:25:1 - error TS6200: Definitions of the following identifiers conflict with those in another file: ImplementationCallback, Func, Constructor, ExpectedRecursive, Expected, SpyObjMethodNames, CustomEqualityTester, CustomMatcherFactory, ExpectationFailed, SpecFunction, SpyObj, jasmine 25 type ImplementationCallback = jasmine.ImplementationCallback; ~~~~
Then I get two errors:
Chrome 78.0.3904 (Mac OS X 10.15.1) SearchComponent should create FAILED Failed: this.getSpy is not a function at <Jasmine>
And:
NullInjectorError: R3InjectorError(DynamicTestModule)[SearchService -> HttpClient -> HttpClient]: NullInjectorError: No provider for HttpClient! error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'SearchService', 'HttpClient', 'HttpClient' ] })
Any idea why this works in Angular 7 and not in Angular 9?
The app that works with Angular 7 is on GitHub at https://github.com/mraible/ng-demo.
-
Xesenix over 4 yearswhy are you creating Mocks on your own instead of using:
{ provide: SearchService, useClass: MockSearchService }
andRouterTestingModule
angular.io/api/router/testing/RouterTestingModule -
Matt Raible over 4 yearsI need to set it myself so I can refer to it later in tests. For example:
expect(mockSearchService.searchSpy).toHaveBeenCalledWith('M');
-
Xesenix over 4 yearsyou can do the same whe you take out them from TesBed like this:
const someMock = TestBed.get<MockSearchService>(SearchService)
or viainject([SearchService], (searchService: MockSearchService) => {...})
-
Matt Raible over 4 yearsUnfortunately, this doesn't help. Using
useClass
has the same results. Here's my test: gist.github.com/mraible/6799ef0f466ff062f47c5c86b942a80b. It still results inTypeError: this.getSpy is not a function
. I'm not sure where this is coming from. I tried renaming theget()
method in my service togetById()
, but the error stays the same. -
Xesenix over 4 yearsthis error
R3InjectorError(DynamicTestModule)[SearchService -> HttpClient -> HttpClient]:
probably could be solved with use ofHttpTestingModule
angular.io/api/common/http/testing/HttpClientTestingModule But i have hard time understanding why you need that extendingSpyObject
or thatSpyObject
at all. -
Xesenix over 4 yearsOk i think I have now wage idea why I that whole
getSpy
looks like something that is somwhere inside jasmine so maybe they changed versions?
-