I am suffering with an issue on nestjs authomatic dependecy injection that I can't resolve.
The problem is when I am running a unit test of a service, the repository is undefined
inside the service! But when I run the service, the application works fine!
I am using vitest to run the tests and pnpm to manage the packages! The repository, and the details to reproduce de example is on this github
I have a simple abstract class called UserRepository
and two implementations, PrismaUserRepository
for running production code that deals with a real database and LocalUserRepository
that only implements the methods without any dependency.
export abstract class UserRepository {
abstract getById(id: string): Promise<User | undefined>;
}
Prisma implementation:
@Injectable()
export class PrismaUserRepository implements UserRepository {
constructor(private repository: RemoteRepository) {}
async getById(id: string): Promise<User | undefined> {
const rawUser = await this.repository.rawUser.findUnique({
where: { id },
});
if (rawUser) {
return new User(rawUser);
}
}
}
Local implementation for tests:
@Injectable()
export class LocalUserRepository implements UserRepository {
async getById(_id: string): Promise<User | undefined> {
return {} as User;
}
}
My service is called GetUserUseCase
and only depends of the UserRepository
@Injectable()
export class GetUserUseCase {
constructor(private readonly userRepository: UserRepository) {}
async execute({ userId }: GetUserRequest): Promise<User> {
const user = await this.userRepository.getById(userId);
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
}
I have a controller with a Get route with a hard coded userId
of "123" and the API is working!
The app.module is like this:
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env'],
}),
],
controllers: [UserController],
providers: [
GetUserUseCase,
RemoteRepository,
{ provide: UserRepository, useClass: PrismaUserRepository },
],
})
And now, the problem! My unit test is broken! The repository is not getting injected when the GetUserUseCase
service runs
Test setup:
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
{
provide: UserRepository,
useClass: LocalUserRepository,
},
GetUserUseCase,
],
}).compile();
userRepository = moduleRef.get<UserRepository>(UserRepository);
spyUserRepositoryGetById = vi.spyOn(userRepository, 'getById');
getUserUseCase = moduleRef.get<GetUserUseCase>(GetUserUseCase);
});
I can make the tests run with two approachs, but I do not want to make those ways, because for my real codebase, it will make me and my team suffer a lot!
The first and easiest way to make it run, is to instanciate the service passing the UserRepository
as a dependency:
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
{
provide: UserRepository,
useClass: LocalUserRepository,
},
GetUserUseCase,
],
}).compile();
userRepository = moduleRef.get<UserRepository>(UserRepository);
spyUserRepositoryGetById = vi.spyOn(userRepository, 'getById');
// getUserUseCase = moduleRef.get<GetUserUseCase>(GetUserUseCase);
getUserUseCase = new GetUserUseCase(userRepository);
});
It is not good for a big codebase, because I have so many services and I will need to do this in each test 😞
The other way to make this work is puting a injection decorator inside the service, and use the provide with it's name: Service code:
@Injectable()
export class GetUserUseCase {
constructor(
@Inject(UserRepository.name)
private readonly userRepository: UserRepository,
) {}
And the test file will look like this:
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
{
provide: UserRepository.name,
useClass: LocalUserRepository,
},
GetUserUseCase,
],
}).compile();
userRepository = moduleRef.get<UserRepository>(UserRepository.name);
spyUserRepositoryGetById = vi.spyOn(userRepository, 'getById');
// getUserUseCase = moduleRef.get<GetUserUseCase>(GetUserUseCase);
getUserUseCase = new GetUserUseCase(userRepository);
});
Note that i have put UserRepository.name
in the provide
and in the moduleRef.get
parameters. And to the server runs correctly, I have to make the same in the app.module.ts
I have tried using useValue
and mocked the functions instead of using useClass
but did not work as well!
So this is what I have to show. I realy want to make the unit tests work only doing a simple configuration using Test.createTestingModule
because doing this way the codebase will only have one big Test.createTestingModule
with all the configuration!
vitest
by default uses esbuild
as a Typescript interpreter under the hood. This is great and fast but esbuild doesn't support emitDecoratorMetadata
which Nest extensively uses to know how to inject providers into each other. To fix this, you can tell vitest
to use @swc/core
through a plugin, which does support the emitDecoratorMetadata
option, allowing vitest to still be the test runner of choice.