I’m writing unit tests for a sandbox project to test a few things in NestJS. I’ve run into an issue where a class that takes an injected service (authService
) is showing up as undefined when being mocked in unit tests. However, the moment I inject it using provider tokens, the service seems to get mocked properly and doesn’t show up as undefined.
I imagine that this issue is with me not making the mock module properly because BOTH approaches work just find in practice (outside of unit tests). It’s only during testing that I can only mock using provider tokens otherwise the authService
in AuthImplementation
shows up as undefined.
Here is my module – auth.module.ts
@Module({
imports: [DatabaseModule],
controllers: [AuthController],
providers: [
{
provide: 'UserRepository',
useFactory: (databaseAdapterService: DatabaseAdapterService) =>
databaseAdapterService.getUserRepository(),
inject: [DatabaseAdapterService],
},
{
provide: AuthService,
useFactory: (userRepository: UserRepository) => {
return new AuthService(userRepository);
},
inject: ['UserRepository'],
},
{
provide: AuthImplementation,
useFactory: (authService: IAuthService) =>
new AuthImplementation(authService),
inject: [AuthService],
},
]
})
The AuthImplementation class is the following – auth.implementation.ts
import { Inject } from '@nestjs/common';
import { User } from 'src/domain/entities/user.entity';
import { AUTH_SERVICE, IAuthService } from 'src/domain/ports/auth-service.interface';
export class AuthImplementation {
constructor(@Inject(AUTH_SERVICE) private readonly authService: IAuthService) { }
// This approach, without the provider token, fails the unit tests because
// authService shows up as undefined when mocked
// constructor(private readonly authService: IAuthService) { }
async register(username: string, password: string): Promise<User> {
return this.authService.register(username, password);
}
async login(username: string, password: string): Promise<User | null> {
return this.authService.login(username, password);
}
}
And the unit test that only works when the provide
field for the mockAuthService
is defined with a provider token looks as the following – auth.implementation.spec.ts
describe('AuthImplementation', () => {
let authImplementation: AuthImplementation;
let mockAuthService: jest.Mocked<IAuthService>;
beforeEach(async () => {
mockAuthService = {
register: jest.fn(),
login: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthImplementation,
{
// NOTE: This approach does not work
//provide: AuthService,
// NOTE: This works just fine
provide: AUTH_SERVICE,
useValue: mockAuthService,
},
],
}).compile();
authImplementation = module.get<AuthImplementation>(AuthImplementation);
});
describe('register', () => {
it('should register a user and return the user object', async () => {
const username = 'testuser';
const password = 'testpass';
const mockUser = new User(new ObjectId(), username, password);
mockAuthService.register.mockResolvedValue(mockUser);
const result = await authImplementation.register(username, password);
expect(mockAuthService.register).toHaveBeenCalledWith(username, password);
expect(result).toBe(mockUser);
});
});
So my question is that, instead of having to define a provider token such as export const AUTH_SERVICE = Symbol('AUTH_SERVICE');
and using it when injecting the auth service, is there any way to directly refer to the AuthService in the testing module? Why can I get away with:
provide: AUTH_SERVICE,
but not
provide: AuthService,
which yields as undefined in the AuthImplementation when logging authService
?