單元測試中的坑
官方例子
我們先來看官方給出的測試用例
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
describe('Cats', () => {
const catsService = { findAll: () => ['test'] };
let app: INestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();
app = module.createNestApplication();
await app.init();
});
it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: catsService.findAll(),
});
});
afterAll(async () => {
await app.close();
});
});
這裡面有幾個坑,稍有不注意可能就掉進去了。首先官方的給出的這個例子,是沒有依賴資料庫的,還有在引入
CatsService
時,沒有依賴原有的,而是 mock 了一個
catsService
,是以後面的測試用例,實在調用這個
catsService
的,而不是 cats 子產品中的。
踩坑
我照着官方給出的例子,自己寫了一個 demo,唯一與官方不同的是,我的 CatsService 是依賴資料庫的,我建立 CatsSchema ,CatsService 中的方法傳回的資料都是從資料庫中查詢出來的。這是我剛開始寫的測試用例。
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { ApiErrorCode } from '../src/common';
import { INestApplication } from '@nestjs/common';
import { CatsModule } from '../src/api/cats/cats.module';
describe('CatsController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [CatsModule],
})
.compile();
app = module.createNestApplication();
await app.init();
});
it('/POST /api/cats', done => {
request(app.getHttpServer())
.post('/cats')
.send({
name: '咪咪',
age: 2,
breed: '英短',
})
.expect(201)
.end((error, response) => {
if (error) {
return done.fail(error);
}
expect(response.body.code).toEqual(ApiErrorCode.SUCCESS);
expect(response.body).not.toBeNull();
done();
});
});
afterAll(async () => {
await app.close();
});
});
這是我剛開始寫的測試用例,每次都跑不通,報錯,報下面這個錯
Nest can't resolve dependencies of the CatModel (?). Please make sure that the argument at index [0] is available in the MongooseModule context.
我一看,感覺應該是沒有加載到
Cat
的
Model
,應為
CatsService
依賴資料庫,要注入
CatsModel
,是以在跑單測的時候老是報這個錯,後來檢視了很多資料,也包括
Nest.js
的官方
issue
,很多人也碰到了這個問題。
看到很多人說在進行單元測試的時候,不應該依賴資料庫,因為很多時候方法調用之間有很多依賴,在執行單測的時候,應該使用 mock ,來模拟依賴,這樣就降低了之間的耦合度,效率也高了很多,不過我看了很多資料,也沒搞明白到底該怎麼樣去寫單測,這個 mock 的用法也沒搞明白。
針對上面的例子,官方很多說應該模拟
schema
,而不是依賴
CatsService
,不過我還是沒搞懂。
有人感興趣的話可以看看下面的連結。
https://github.com/nestjs/nest/issues/363
如果有大佬能給出一個完整的示例就好了,我看完上面的讨論還是雲裡霧裡,沒有特别明白,隻是知道了不應該依賴
CatsService
,要
mock
一個,沒有例子,自己寫出來的跑不通啊。
解決方法
曆經千辛萬苦找到了一個解決方案,下面貼出代碼,供大家參考。
// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { CatSchema } from './schema/cats.schema';
@Module({
imports: [
MongooseModule.forFeature([{ name: 'Cat', schema: CatSchema }]),
],
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
// cats.controller.ts
import { Controller, Post, Body, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { resFormat, ApiCode, ApiErrorCode } from '../../common';
@Controller('cats')
export class CatsController {
constructor(private readonly catService: CatsService) {}
@Post()
async create(@Body() entity: CreateCatDto) {
this.catService.create(entity);
return resFormat(ApiCode.POST_CAT, ApiErrorCode.SUCCESS, '新增貓成功', null);
}
}
// cats.service.ts
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { Cat } from './interfaces/cat.interface';
import { CreateCatDto } from './dto/create-cat.dto';
import { InjectModel } from '@nestjs/mongoose';
@Injectable()
export class CatsService {
constructor(@InjectModel('Cat') private readonly catModel: Model<Cat>) {}
async create(createCatDto: CreateCatDto): Promise<Cat> {
const createdCat = new this.catModel(createCatDto);
return await createdCat.save();
}
}
// cats.e2e-spec.ts
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { ApiErrorCode } from '../src/common';
import { INestApplication } from '@nestjs/common';
import { CatsModule } from '../src/api/cats/cats.module';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsService } from '../src/api/cats/cats.service';
describe('CatsController (e2e)', () => {
let app: INestApplication;
// 這裡是對 CatsService 的模拟
const catsService = {
// 這種寫法還沒了解,也是參考别人的例子,不知道應該怎麼模拟裡面具體的邏輯
create: () => ({}),
findAll: () => ({}),
};
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [
CatsModule,
MongooseModule.forRootAsync({
useFactory: () => ({
uri: 'mongodb://localhost/blog',
}),
}),
],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();
app = module.createNestApplication();
await app.init();
});
it('/POST /api/cats', done => {
request(app.getHttpServer())
.post('/cats')
.send({
name: '咪咪',
age: 2,
breed: '英短',
})
.expect(201)
.end((error, response) => {
if (error) {
return done.fail(error);
}
// 因為沒有依賴資料庫,是以測試資料自己模拟
// 怎麼模拟傳回結果,還不會
expect(response.body.code).toEqual(ApiErrorCode.SUCCESS);
expect(response.body).not.toBeNull();
done();
});
});
afterAll(async () => {
await app.close();
});
});
這樣寫出來的單元測試總算是能夠跑通了,希望有的大佬在看了這篇文章的時候,能夠貼出一個完整的測試用例,包括怎麼模拟傳回結果,不依賴方法調用裡面的依賴應該怎麼寫,有沒有更優雅的方式。
整個 demo 寫下來,還是沒有完全了解 Nest.js 單元測試,感覺其中最重要的是不依賴資料庫等其他依賴項,就能寫好的單元測試。