You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 18:07:02