Automatically mock your Angular Services when Testing
In this blog post, I want to describe how you can automatically mock your Angular Services while Testing.
How can we auto mock our dependencies when testing?
When we are testing in Angular either with jest or with karma/jasmine, we can use Angular’s dependency injection to switch services with any mock services under the hood and to control http-calls and other method calls with this.
describe('XyzService with service mock', () => {
@Injectable()
class MyServiceMock {
getName(): Observable<string> {
return timer(500).pipe(mapTo('Mock Fabian'));
}
}
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{ provide: MyService, useClass: MyServiceMock }],
});
service = TestBed.inject(MyService);
});
});
}
While this is absolutely fine and works, it might get really cumbersome to write a mock service for every service we write.
We can automate this and make it a little more convenient when working with mocks in our services. I will use jest in the sample here, but you can modify this to your needs.
First, we are going to implement an autoMock
method which is generic, accepting a Service returning the service as well. As a result, to return we are going to create an empty object which we will fill up with the properties later.
export function autoMock<T>(obj: new (...args: any[]) => T): T {
const res = {} as any;
// ADD CODE HERE
return res;
}
Now we could iterate over all properties of the source class and mock them, but we have to be careful. To mock typescript properties, we have to mark them as configurable: true
to make this work. This also means that we have to see which one is actually a function and which property is not!
export function autoMock<T>(obj: new (...args: any[]) => T): T {
const res = {} as any;
const keys = Object.getOwnPropertyNames(obj.prototype);
const allMethods = keys.filter((key) => {
try {
return typeof obj.prototype[key] === 'function';
} catch (error) {
return false;
}
});
const allProperties = keys.filter((x) => !allMethods.includes(x));
// Do the mock work
return res;
}
Alright, now we know which property of the class to mock is a function and which one is a “real” property.
Now we can do the mock work and set this mocks on the empty object to return.
export function autoMock<T>(obj: new (...args: any[]) => T): T {
const res = {} as any;
const keys = Object.getOwnPropertyNames(obj.prototype);
const allMethods = keys.filter((key) => {
try {
return typeof obj.prototype[key] === 'function';
} catch (error) {
return false;
}
});
const allProperties = keys.filter((x) => !allMethods.includes(x));
// Mocking with jest here, modify to your needs if needed
allMethods.forEach((method) => (res[method] = jest.fn()));
allProperties.forEach((property) => {
Object.defineProperty(res, property, {
get: function () {
return '';
},
configurable: true,
});
});
return res;
}
Now that is basically it, we could use this straight away referring to our first sample like this:
describe('XyzService with service mock', () => {
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{ provide: MyService, useClass: autoMock(MyService) }],
});
service = TestBed.inject(MyService);
});
});
}
But that is not as beautiful as we want it, we can do better. Let’s move the provide-syntax into our mock helper and use it then
export function autoMock<T>(obj: new (...args: any[]) => T): T {
//...
}
export function provideMock<T>(type: new (...args: any[]) => T): Provider {
const mock = autoMock(type);
return { provide: type, useValue: mock };
}
And with that we can now use the provideMock
method in our tests
describe('XyzService with service mock', () => {
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ provideMock(MyService),],
});
service = TestBed.inject(MyService);
});
});
it('should dosomething correct', () => {
const spy = jest.spyOn(service, 'myMethod');
// act
// ...
// assert
// ...
});
}
With this, the Service gets mocked automatically and is always in sync with your real implementation.
Hope this helps