How to Implement Mock With Spectator With Jest
How to Implement Mock With Spectator With Jest
What I learned about Mocking with Spectator Tests
Source: Unsplash
I mentioned in myprevious blog postthat Spectator is currently my go-to test tool for my Angular Apps. In the past couple of weeks, I’ve learned a few things about mocking using Spectator which I hope you will find useful.
Note that I’m using Spectator with Jest.
Remove unused providers
In mycreateServiceFactory
orcreateComponentFactory
, I have declared my dependencies to be automatically mocked but I still kept the providers withuseValue: {}
. In the example below, myUserApiService
is already automatically mocked. In this case, it’s no longer necessary to declareUserApiService
in the providers withuseValue: {}
:
let spectator: SpectatorService<UserService>; const createService = createServiceFactory({ service: UserService, providers: [ { provide: UserApiService, useValue: {}}, // TODO: Remove, we don't need it. ], mocks: [UserApiService], // Automatically mock }); beforeEach(() => (spectator = createService()));
We don’t needUserApiService
in the providers:
let spectator: SpectatorService<UserService>; const createService = createServiceFactory({ service: UserService, mocks: [UserApiService], }); beforeEach(() => (spectator = createService()));
KeepingUserApiService
in the providers doesn’t have any side-effect. However, it’s best to remove it if it’s not used.
andReturn() vs mockReturnValue()
I’ve been exclusively usingandReturn()
to mock methods within a test case since I started using Spectator. I only found out recently about Jest’smockReturnValue()
. It works the same way asandReturn()
. The main difference I noticed is thatandReturn()
is not strict with its return type.
Given a method that returns the typeObservable<User>
.
class UserService { getUser(): Observable<User> { // code here.. }}
WhereUser
is:
interface User { firstName: string; lastName: string;}
UsingmockReturnValue
, I will get an error if I don’t mock using the expected return type:
const userUservice = spectator.inject(UserService);userService.getUser.mockReturnValue(of('user mock'));
Error:
error TS2345: Argument of type 'Observable<string>' is not assignable to parameter of type 'Observable<User>'.
mockReturnValue()
accepts the method’s declared return type only:
const userUservice = spectator.inject(UserService);userService.getUser.mockReturnValue(of({firstName: 'First', lastName: 'Last'} as User));
UsingandReturn()
, I can use a different type:
const userUservice = spectator.inject(UserService);userService.getUser.andReturn(of('user mock')); // I can mock with a string type!
Mocking by direct assignment
I have usedandReturn()
(and soonmockReturnValue()
) in test cases if I care about the return value of the mock. I find it easier to spot when I’m looking for the mocks that are happening in a single test.
However, there are test cases where I’m only interested in assertingtoHaveBeenCalled()
to a mocked method. In that case, I’d usually assignjest.fn()
to the method that I want to assert. This only works if the property or method isnotread-only.
If I’m usingUserService
in my component.
constructor(private userService: UserService) {}
I can mock itsgetUser()
method call in a test by assigning the mock function directly. Then assert with atoHaveBeenCalled()
:
spectator.component['userService'].getUser = jest.fn();//.. some code hereexpect(spectator.component['userService'].getUser).toHaveBeenCalled();
Mocking read-only properties
What if the method I want to mock is read-only?
I have a getter in my service:
export class UserService { //.. some code get canAccess$(): Observable<boolean> { // .. implementaion }}
I have a component that uses the above getterUserService.canAccess$
. When I try to mock that getter usingandReturn()
:
const userService = spectator.inject(UserService);userService.canAccess$.andReturnvalue(of(false));
I get the following error:
TS2339: Property 'andReturn' does not exist on type 'Observable '.
I also can’t assign the mock directly becausecanAccess$
here is read-only:
spectator.component['userService'].canAccess$ = of(false);
Cannot assign to 'canAccess$' because it is a read-only property.
There are a few ways to handle this.
Using Object.defineProperty()
I previously usedObject.defineProperty
to modify the service object’s property:
Object.defineProperty(spectator.component['userService'], 'canAccess$', {
value: of(true)
});
This worked. I thought there must be a way to achieve this by using Spectator rather than modifying the service object directly, see the next section.
Setting useValue in a test case
I found out about this approach when I was browsing throughthe examplesin Spectator’s README.
I can declare a default mock forcanAccess$
in mycreateComponentFactory
call by settinguseValue
.
const createComponent = createComponentFactory({
component: MyComponent,
//...typeOrOptions here
providers: [
{
provide: UserService,
useValue: {
canAccess$: of(true)
}
}
],
});
All tests in a suite will use this default value ofcanAccess$
unless I override it within a test or another test suite. To override the default mock, I can specify the provider in the test case with auseValue
. In this example, I change the return value toof(false)
.
const provider = {
provide: UserService,
useValue: {
canAccess: of(false)
},
};
Then callcreateComponent()
using the provider with the mock override that I just declared.
spectator = createComponent({
providers: [provider],
});
I can override the default mock in individual test cases.
it('should prevent access...', () => {
const provider = {
provide: UserService,
useValue: {
canAccess$: of(false)
},
};
spectator = createComponent({
providers: [provider],
});
//.. code here
});
Or I can declare the override inbeforeEach()
if I want to use it in a test suite:
describe('Prevent access', () => {
beforeEach(() => {
const provider = {
provide: UserService,
useValue: {
canAccess$: of(false)
},
};
spectator = createComponent({
providers: [provider],
});
//.. code here
});
it('should prevent access...', () => {
// .. code here
});
// .. more tests
});
If you like this story, you might also enjoy my other stories about Spectator Test and Angular: