typescript - Nestjs injicerar inte repository-beroende vid testning med Test.createTestingModule
Jag lider av ett problem med NestJS automatisk beroendeinjektion som jag inte kan lösa. Problemet är att när jag kör ett enhetstest av en tjänst, är repository undefined inuti tjänsten! Men när jag kör tjänsten fungerar applikationen bra!
Jag använder vitest för att köra testerna och pnpm för att hantera paketen! Repositoriet och detaljerna för att reproducera exemplet finns på denna GitHub
Jag har en enkel abstrakt klass som heter UserRepository och två implementationer, PrismaUserRepository för att köra produktionskod som hanterar en riktig databas och LocalUserRepository som endast implementerar metoderna utan några beroenden.
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);
}
}
}
Lokal implementering för tester:
@Injectable()
export class LocalUserRepository implements UserRepository {
async getById(_id: string): Promise<User | undefined> {
return {} as User;
}
}
Min tjänst kallas GetUserUseCase och beror endast på 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;
}
}
Jag har en controller med en Get-rutt med en hårdkodad userId på "123" och API:et fungerar!
App.module ser ut så här:
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env'],
}),
],
controllers: [UserController],
providers: [
GetUserUseCase,
RemoteRepository,
{ provide: UserRepository, useClass: PrismaUserRepository },
],
})
Och nu, problemet! Mitt enhetstest är trasigt! Repositoriet injiceras inte när GetUserUseCase-tjänsten körs
Testuppställning:
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);
});
Felet:
Jag kan få testerna att köras med två tillvägagångssätt, men jag vill inte göra på de sätten, för för min riktiga kodbas kommer det att få mig och mitt team att lida mycket!
Det första och enklaste sättet att få det att köras är att skapa en instans av tjänsten och skicka UserRepository som ett beroende:
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);
});
Det är inte bra för en stor kodbas, eftersom jag har så många tjänster och jag kommer att behöva göra detta i varje test 😞
Det andra sättet att få detta att fungera är att sätta en injektionsdekoration inuti tjänsten och använda provide med dess namn: Tjänstkoden:
@Injectable()
export class GetUserUseCase {
constructor(
@Inject(UserRepository.name)
private readonly userRepository: UserRepository,
) {}
Och testfilen kommer att se ut så här:
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);
});
Observera att jag har satt UserRepository.name i provide och i moduleRef.get-parametrarna. Och för att servern ska köras korrekt måste jag göra samma sak i app.module.ts
Jag har försökt använda useValue och mocka funktionerna istället för att använda useClass, men det fungerade inte heller!
Så detta är vad jag har att visa. Jag vill verkligen få enhetstesterna att fungera genom att bara göra en enkel konfiguration med Test.createTestingModule eftersom genom att göra på det här sättet kommer kodbasen bara ha ett stort Test.createTestingModule med all konfiguration!
Svar #1
vitest använder som standard esbuild som Typescript-tolk under huven. Detta är bra och snabbt men esbuild stöder inte emitDecoratorMetadata som Nest använder omfattande för att veta hur man injicerar providers i varandra. För att fixa detta kan du säga åt vitest att använda @swc/core via ett plugin, vilket stöder emitDecoratorMetadata-alternativet, vilket gör att vitest fortfarande kan vara testköraren som används.
Dokumentation om installationen finns i Nests dokumentation

