Nest Test Code - boostcampwm2023/web07-GBS GitHub Wiki

์„ค์น˜

$ npm i --save-dev @nestjs/testing

Unit Testing.

Testing files should have a .spec or .test suffix.

isolated testing

  • ํ”„๋ ˆ์ž„์›Œํฌ๋กœ๋ถ€ํ„ฐ ๋…๋ฆฝ์ 
  • dependency injection ๊ฐ€ ์—†๋‹ค
import { CatsController } from "./cats.controller";
import { CatsService } from "./cats.service";

describe("CatsController", () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(() => {
    catsService = new CatsService();
    catsController = new CatsController(catsService);
  });

  describe("findAll", () => {
    it("should return an array of cats", async () => {
      const result = ["test"];
      jest.spyOn(catsService, "findAll").mockImplementation(() => result);

      expect(await catsController.findAll()).toBe(result);
    });
  });
});

Testing Utilities

@nestjs/testing : ๊ฐ•๋ ฅํ•œ ํ…Œ์ŠคํŠธ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ

import { Test } from "@nestjs/testing"; //
import { CatsController } from "./cats.controller";
import { CatsService } from "./cats.service";

describe("CatsController", () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      controllers: [CatsController],
      providers: [CatsService],
    }).compile();

    catsService = moduleRef.get<CatsService>(CatsService);
    catsController = moduleRef.get<CatsController>(CatsController);
  });

  describe("findAll", () => {
    it("should return an array of cats", async () => {
      const result = ["test"];
      jest.spyOn(catsService, "findAll").mockImplementation(() => result);

      expect(await catsController.findAll()).toBe(result);
    });
  });
});

Test

  • ์ „์ฒด Nest ๋Ÿฐํƒ€์ž„์„ mokeํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์ปจํ…์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • moke ๋ฐ overriding์„ ํฌํ•จํ•˜์—ฌ ํด๋ž˜์Šค ์ธ์Šคํ„ด์Šค๋ฅผ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํ›„ํฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • createTestingModule() :
    • ๋ชจ๋“ˆ metadata object๋ฅผ ์ธ์ˆ˜๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. (@Module() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์— ์ „๋‹ฌํ•œ ๊ฒƒ๊ณผ ๋™์ผํ•œ object).
    • TestingModule ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • compile() :
    • ์ด ๋ฐฉ๋ฒ•์€ ์ข…์†์„ฑ์ด ์žˆ๋Š” ๋ชจ๋“ˆ์„ bootstrap ํ•ฉ๋‹ˆ๋‹ค. (NestFactory.create()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ธฐ์กด main.ts ํŒŒ์ผ์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ถ€ํŠธ์ŠคํŠธ๋žฉํ•˜๋Š” ๋ฐฉ์‹๊ณผ ์œ ์‚ฌ).
    • ํ…Œ์ŠคํŠธ ์ค€๋น„๊ฐ€ ๋œ ๋ชจ๋“ˆ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    • asynchronous โ‡’ await ๋ฐ˜ํ™˜
    • ๋ชจ๋“ˆ์ด ์ปดํŒŒ์ผ๋˜๋ฉด get() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„ ์–ธ๋œ ์ •์  ์ธ์Šคํ„ด์Šค(์ปจํŠธ๋กค๋Ÿฌ ๋ฐ ๊ณต๊ธ‰์ž)๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Auto moking - ๊ณต๋ถ€ ํ•„์š”

  • ๋งŽ์€ ์˜์กด์„ฑ์ด ์žˆ์„ ๋•Œ ์œ ์šฉ
  • ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด createTestingModule()์„ useMocker() ๋ฉ”์„œ๋“œ์™€ ์—ฐ๊ฒฐํ•˜์—ฌ ์ข…์†์„ฑ ๋ชจ์˜ ๊ฐ์ฒด์— ๋Œ€ํ•œ ํŒฉํ† ๋ฆฌ๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ํŒฉํ† ๋ฆฌ๋Š” ์ธ์Šคํ„ด์Šค ํ† ํฐ์ธ ์„ ํƒ์  ํ† ํฐ, Nest ๊ณต๊ธ‰์ž์—๊ฒŒ ์œ ํšจํ•œ ๋ชจ๋“  ํ† ํฐ์„ ๊ฐ€์ ธ์™€ ๋ชจ์˜ ๊ตฌํ˜„์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” jest-mock์„ ์‚ฌ์šฉํ•˜์—ฌ ์ผ๋ฐ˜ ๋ชจ์ปค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  jest.fn()์„ ์‚ฌ์šฉํ•˜์—ฌ CatsService์— ๋Œ€ํ•œ ํŠน์ • ๋ชจ์ปค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์˜ˆ์ž…๋‹ˆ๋‹ค.
import { ModuleMocker, MockFunctionMetadata } from "jest-mock";

const moduleMocker = new ModuleMocker(global);

describe("CatsController", () => {
  let controller: CatsController;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      controllers: [CatsController],
    })
      .useMocker((token) => {
        const results = ["test1", "test2"];
        if (token === CatsService) {
          return { findAll: jest.fn().mockResolvedValue(results) };
        }
        if (typeof token === "function") {
          const mockMetadata = moduleMocker.getMetadata(
            token
          ) as MockFunctionMetadata<any, any>;
          const Mock = moduleMocker.generateFromMetadata(mockMetadata);
          return new Mock();
        }
      })
      .compile();

    controller = moduleRef.get(CatsController);
  });
});

**End-to-end testing**

  • ๋ณด๋‹ค ์ข…ํ•ฉ์ ์ธ ์ˆ˜์ค€์—์„œ ํด๋ž˜์Šค์™€ ๋ชจ๋“ˆ์˜ ์ƒํ˜ธ ์ž‘์šฉ Test
  • end User โ†โ†’ production
import * as request from "supertest";
import { Test } from "@nestjs/testing";
import { CatsModule } from "../../src/cats/cats.module";
import { CatsService } from "../../src/cats/cats.service";
import { INestApplication } from "@nestjs/common";

describe("Cats", () => {
  let app: INestApplication;
  let catsService = { findAll: () => ["test"] };

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [CatsModule],
    })
      .overrideProvider(CatsService)
      .useValue(catsService)
      .compile();

    app = moduleRef.createNestApplication();
    await app.init();
  });

  it(`/GET cats`, () => {
    return request(app.getHttpServer()).get("/cats").expect(200).expect({
      data: catsService.findAll(),
    });
  });

  afterAll(async () => {
    await app.close();
  });
});

**NestJs TypeORM ํ…Œ์ŠคํŠธ ์ฝ”๋“œ**

Fixtures

  • Fixture๋ž€
    • Test Fixture : ๊ณ ์ •๋˜์–ด ์žˆ๋Š” ๋ฌผ์ฒด ๋ฅผ ์˜๋ฏธ
    • ํ…Œ์ŠคํŠธ ์‹คํ–‰์„ ์œ„ํ•ด ๋ฒ ์ด์Šค๋ผ์ธ์œผ๋กœ์„œ ์‚ฌ์šฉ๋˜๋Š” ๊ฐ์ฒด๋“ค์˜ ๊ณ ์ •๋œ ์ƒํƒœ
    • ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๊ธฐ๋ณธ์œผ๋กœ ์„ธํŒ…๋œ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ
beforeEach(() => {
	setFixtures(...);
});

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ beforeEach๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „ ์ž‘์—…์„ ๋ช…์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. beforeEach์—์„œ setFixtures ํ•จ์ˆ˜๋ฅผ ๊ตฌ์„ฑํ•ด ์คŒ์œผ๋กœ์จ ํ…Œ์ŠคํŠธ์ผ€์ด์Šค๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „ ์ •ํ•ด๋‘” ๋ฐ์ดํ„ฐ๋กœ db๋ฅผ ์ดˆ๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋Š” ๋™์ผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚œ ํ›„ afterEach์—์„œ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹น ๋‚ ๋ ค์ค๋‹ˆ๋‹ค.

afterEach(() => {
  truncate all tables..
})

Ex)

// test.spec.ts
setFixtures([[FooRepository, FooEntities]]);

// setFixtures ๋‚ด๋ถ€
// beforeEach
await repository.runTransaction(async (tx) => {
  await repository.rawQuery('set foreign_key_checks = 0', [], tx);
  await repository.insert(JSON.parse(JSON.stringify(data)), tx);
  await repository.rawQuery('set foreign_key_checks = 1', [], tx);
});

// afterEach
await repository.runTransaction(async (tx) => {
    await repository.rawQuery('set foreign_key_checks = 0', [], tx);
    await repository.rawQuery(`truncate ${repository.getTableName()}`, [], tx);
    await repository.rawQuery('set foreign_key_checks = 1', [], tx);
  });
}

**Docker๋ฅผ ์ด์šฉํ•œ ๋กœ์ปฌ mysql ์„ค์น˜**

  • ์™œ local์— mysql์„ ์„ค์น˜ํ•˜์ง€ ์•Š๋‚˜์š”?
  • ์™œ dev์„œ๋ฒ„์— db๋กœ ์—ฐ๊ฒฐํ•˜์ง€ ์•Š๋‚˜์š”?

์šฐ์„  ๊ผญ docker๊ฐ€ ์•„๋‹ˆ์–ด๋„ ๋ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ์— mysql์„ ์ง์ ‘ ์„ค์น˜ํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์ž๋Š” ๊ผญ mysql์˜ ์„ค์น˜์— ์ต์ˆ™ํ•œ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์•„๋‹ ์ˆ˜ ์žˆ์Œ์— ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ๊ฒฝ์šฐ๊ฐ€ ์žˆ๊ณ , ๊ทธ๋Ÿฐ๊ฒฝ์šฐ๋ผ๋ฉด ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋„ mysql์„ค์น˜๋ฅผ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

๋ณดํ†ต E2E๋ผ๊ณ  ํ•˜๋ฉด controller <โ€”> db ๊นŒ์ง€์˜ ํ…Œ์ŠคํŠธ๋ผ๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ข€ ๋” ํฌ๊ฒŒ E2E๋ฅผ ๋ณธ๋‹ค๋ฉด ํ”„๋ก ํŠธ์—”๋“œ <โ€”> ๋ฐฑ์—”๋“œ๊นŒ์ง€์˜ ๋ฒ”์œ„๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๊ฒฝ์šฐ ํ”„๋ก ํŠธ์—์„œ ํŠน์ • ๋ฒ„ํ„ด์„ ๋ˆ„๋ฅด๊ณ  api ๊ฐ€ ํ˜ธ์ถœ๋˜์–ด db์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ ๊นŒ์ง€ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

(js์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋„๊ตฌ๋กœ๋Š” cypress์™€ ๊ฐ™์€๊ฒŒ ์žˆ์Šต๋‹ˆ๋‹ค.)

test ํ•  ๋•Œ ์ž๋™์œผ๋กœ docker test database ๋งŒ๋“ค๊ธฐ

server/api-server/testMysql์— dockerfile ์ƒ์„ฑ

FROM mysql:8.2.0

ENV MYSQL_ROOT_PASSWORD=audgml145
ENV MYSQL_DATABASE=gbs
ENV LANG=C.UTF-8

COPY ./docker/mysql/init-test-db.sql /docker-entrypoint-initdb.d/init-test-db.sql
  • yarn test:docker : docker ์ž๋™ ์ƒ์„ฑ ๋ฐ test ์‹คํ–‰
  • cp ./sql/schema.sql ./testMysql/docker/mysql/init-test-db.sq : ์‹ค์ œ db sql์„ test db์˜ ์ดˆ๊ธฐ sql๋กœ ๋ณต์‚ฌ
  • docker build -t testdb ./testMysql : docker image ์ƒ์„ฑ
  • docker run -it -p 3306:3306 testdb : ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ ์‹œ -p ์˜ต์…˜์„ ์ด์šฉํ•ด์„œ ๋ฐ”์ธ๋”ฉํ•  ํฌํŠธ๋ฅผ ๋ถ€์—ฌํ•œ๋‹ค.
  • jest --watch : test ์‹คํ–‰

Ref

https://docs.nestjs.com/fundamentals/testing

https://kyungyeon.dev/posts/85

โš ๏ธ **GitHub.com Fallback** โš ๏ธ