天天看點

Nest.js 單元測試踩坑之旅單元測試中的坑

單元測試中的坑

官方例子

我們先來看官方給出的測試用例

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 單元測試,感覺其中最重要的是不依賴資料庫等其他依賴項,就能寫好的單元測試。

繼續閱讀