NestJS/Vitest中无法Override Service的问题排查求助
NestJS E2E测试中overrideProvider无法Mock ItemsService的问题
我们基于NestJS/Vitest/Prisma实现应用的E2E测试,尝试在ItemsController的测试中用overrideProvider来Mock ItemsService,但操作未生效。认证鉴权参考NestJS官方文档实现,JwtAuthGuard已在AppModule全局启用。
我们使用了overrideProvider(ItemsService)和useValue(itemsServiceMock),但实际执行的是ItemsService的原方法而非模拟对象itemsServiceMock。期望overrideProvider和useValue能生效,让GET /items接口返回预设的items数据,求排查错误。
测试结果
stdout | test/items.e2e-spec.ts > ItemsController (e2e) > 200 /items (GET) ItemsController Called CANNOT override service! Original service is used! ❯ test/items.e2e-spec.ts (2) ❯ ItemsController (e2e) (2) × 200 /items (GET) ✓ 401 /items (GET) FAIL test/items.e2e-spec.ts > ItemsController (e2e) > 200 /items (GET) Error: expected [ ItemDto { id: 1, body: 'test', status: 'TODO', userId: 1, createdAt: 2024-06-09T09:43:14.038Z, updatedAt: 2024-06-09T09:43:14.038Z }, ItemDto { id: 2, body: 'test', status: 'TODO', userId: 1, createdAt: 2024-06-09T09:43:14.038Z, updatedAt: 2024-06-09T09:43:14.038Z } ] response body, got {}
依赖配置(package.json节选)
"dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", "@prisma/client": "^5.15.0", "mysql2": "^3.9.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "prisma": "^5.15.0", "supertest": "^6.3.3", "vitest": "^1.6.0", "vitest-mock-extended": "^1.3.1" }
测试代码(items.e2e-spec.ts)
const date = new Date(); const items = [ new ItemDto(1, 'test', Status.TODO, 1, date, date), new ItemDto(2, 'test', Status.TODO, 1, date, date), ]; describe('ItemsController (e2e)', () => { let app: INestApplication; let jwtService: JwtService; let configService: ConfigService; let prismaServiceMock: DeepMockProxy<PrismaService>; let itemsServiceMock: DeepMockProxy<ItemsService>; const getToken = (payload: object) => { return jwtService.sign(payload, { secret: configService.get<string>('JWT_SECRET'), }); }; beforeEach(async () => { prismaServiceMock = mockDeep<PrismaService>(); itemsServiceMock = mockDeep<ItemsService>(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }) // CANNOT override!!! NOT working. .overrideProvider(ItemsService) .useValue(itemsServiceMock) // Working! .overrideProvider(PrismaService) .useValue(prismaServiceMock) .compile(); app = moduleFixture.createNestApplication(); //app.useLogger(new Logger()); configService = moduleFixture.get<ConfigService>(ConfigService); jwtService = moduleFixture.get<JwtService>(JwtService); await app.init(); }); afterEach(async () => { await app.close(); }); it('200 /items (GET)', async () => { const token = getToken({ sub: 1, username: 'test' }); const expected = items; itemsServiceMock.findAll.calledWith(any()).mockResolvedValue(expected); return request(app.getHttpServer()) .get('/items') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(expected); }); it('401 /items (GET)', () => { return request(app.getHttpServer()).get('/items').expect(401); }); });
模块代码
app.module.ts
@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), ItemsModule, AuthModule, UsersModule, ], controllers: [AppController], providers: [ AppService, { provide: APP_GUARD, useClass: JwtAuthGuard, }, ], }) export class AppModule {}
items.module.ts
@Module({ providers: [ItemsService, PrismaService], controllers: [ItemsController], }) export class ItemsModule {}
业务代码
items.controller.ts
@Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(@Request() req): Promise<ItemDto[]> { console.log('ItemsController Called'); return await this.itemsService.findAll({ userId: req.user.id }); } }
items.service.ts
@Injectable() export class ItemsService { constructor(private readonly prisma: PrismaService) {} async findAll(itemWhereInput: Prisma.ItemWhereInput): Promise<ItemDto[]> { console.log('CANNOT override service! Original service is used!'); return toDtoArray( ItemDto, await this.prisma.item.findMany({ where: itemWhereInput }), ); } }
解决方案
问题出在模块层级的覆盖优先级:你在根模块(AppModule)层面尝试覆盖ItemsService,但ItemsService是在ItemsModule内部声明的提供者。NestJS的模块隔离机制会优先使用子模块内部注册的提供者,而非根模块层面的覆盖。
方法1:直接override子模块的提供者
在测试模块中,不要直接导入AppModule,而是导入ItemsModule并覆盖其内部的ItemsService,同时处理全局依赖:
// 修改items.e2e-spec.ts中的beforeEach部分 beforeEach(async () => { prismaServiceMock = mockDeep<PrismaService>(); itemsServiceMock = mockDeep<ItemsService>(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ItemsModule], providers: [ ConfigModule.forRoot({ isGlobal: true }), JwtModule.register({ secret: 'test-secret' }), { provide: APP_GUARD, useClass: JwtAuthGuard }, ], }) .overrideProvider(ItemsService) .useValue(itemsServiceMock) .overrideProvider(PrismaService) .useValue(prismaServiceMock) .compile(); // 后续代码不变... });
方法2:使用overrideModule覆盖子模块的提供者
如果必须导入AppModule,可通过overrideModule定位到ItemsModule再覆盖内部提供者:
// 修改items.e2e-spec.ts中的beforeEach部分 beforeEach(async () => { prismaServiceMock = mockDeep<PrismaService>(); itemsServiceMock = mockDeep<ItemsService>(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }) .overrideModule(ItemsModule) .useModule( Test.createTestingModule({ providers: [ { provide: ItemsService, useValue: itemsServiceMock }, { provide: PrismaService, useValue: prismaServiceMock }, ], controllers: [ItemsController], }).compile(), ) .compile(); // 后续代码不变... });
额外注意点
- 修正mock方法调用:
itemsServiceMock.findAll.calledWith(any()).mockResolvedValue(expected)改为itemsServiceMock.findAll.mockResolvedValue(expected),或用mockImplementation匹配参数:itemsServiceMock.findAll.mockImplementation((where) => { expect(where).toEqual({ userId: 1 }); return Promise.resolve(expected); }); - 避免类实例序列化问题:将ItemDto实例改为普通对象,防止返回空JSON:
const items = [ { id: 1, body: 'test', status: 'TODO', userId: 1, createdAt: date, updatedAt: date }, { id: 2, body: 'test', status: 'TODO', userId: 1, createdAt: date, updatedAt: date }, ];
内容的提问来源于stack exchange,提问作者uramonk
相关产品推荐
相关产品推荐

