How to Implement Mock With Spectator With Jest | by Ardy Gallego Dedase | Apr, 2022

Photo by Ferenc Almasi on Unsplash

I mentioned in my previous blog post that 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.

In my createServiceFactory or createComponentFactoryI have declared my dependencies to be automatically mocked but I still kept the providers with useValue: {}. In the example below, my UserApiService is already automatically mocked. In this case, it’s no longer necessary to declare UserApiService in the providers with useValue: {}:

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 need UserApiService in the providers:

let spectator: SpectatorService<UserService>;
const createService = createServiceFactory({
service: UserService,
mocks: [UserApiService],
});

beforeEach(() => (spectator = createService()));

Keeping UserApiService in the providers doesn’t have any side-effect. However, it’s best to remove it if it’s not used.

I’ve been exclusively using andReturn() to mock methods within a test case since I started using Spectator. I only found out recently about Jest’s mockReturnValue(). It works the same way as andReturn(). The main difference I noticed is that andReturn() is not strict with its return type.

Given a method that returns the type Observable<User>.

class UserService {
getUser(): Observable<User> {
// code here..
}
}

Where User is:

interface User {
firstName: string;
lastName: string;
}

Using mockReturnValueI 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));

Using andReturn()I can use a different type:

const userUservice = spectator.inject(UserService);
userService.getUser.andReturn(of('user mock')); // I can mock with a string type!

I have used andReturn() (and soon mockReturnValue()) 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 asserting toHaveBeenCalled() to a mocked method. In that case, I’d usually assign jest.fn() to the method that I want to assert. This only works if the property or method is not read-only.

If I’m using UserService in my component.

constructor(private userService: UserService) {}

I can mock its getUser() method call in a test by assigning the mock function directly. Then assert with a toHaveBeenCalled():

spectator.component['userService'].getUser = jest.fn();
//.. some code here
expect(spectator.component['userService'].getUser).toHaveBeenCalled();

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 getter UserService.canAccess$. When I try to mock that getter using andReturn():

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 because canAccess$ 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 used Object.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 through the examples in Spectator’s README.

I can declare a default mock for canAccess$ in my createComponentFactory call by setting useValue.

const createComponent = createComponentFactory({
component: MyComponent,
//...typeOrOptions here
providers: [
{ provide: UserService, useValue: { canAccess$: of(true) } }
],
});

All tests in a suite will use this default value of canAccess$ 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 a useValue. In this example, I change the return value to of(false) .

const provider = {
provide: UserService,
useValue: { canAccess: of(false) },
};

Then call createComponent() 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 in beforeEach() 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:

Leave a Comment