NestJS: Mocking & Spy
๋ณธ ๊ธ์ ์ ๊ฐ NestJS
ํ๋ ์์ํฌ๋ฅผ ํตํด ๊ฐ๋ฐํ๋ฉด์ ๊นจ๋ฌ์ ๋
ธํ์ฐ๋ฅผ ๊ธฐ๋กํ ๊ฒ์
๋๋ค. ์ ๊ฐ ์ ์ํ ๋ฐฉ๋ฒ๋ณด๋ค ๋ ์ข์ ๋ฐฉ๋ฒ์ด ์์ ์๋ ์์ต๋๋ค. ์ง์ ์ ์ธ์ ๋ ํ์์
๋๋ค :)
NestJS
์์ Javascript์ ํ
์คํธ ํ๋ ์์ํฌ์ธ jest
link๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ํ๋ ํ
์คํธ ํ๋ ์์ํฌ๋ฅผ ์ง์ํ๋ค.
๋ฌผ๋ก ์ฝ๊ฐ์ ๋ณํ์ ์๊ฒ ์ง๋ง, ๊ทธ๋ฅ ์ง์๋ง ํ๋ ์์ค์ด ์๋๋ผ NestJS
์ ํ
์คํธ๋ฅผ jest
๋ก ํ๋ค.
npm i --save-dev @nestjs/testing
ํ ์คํ ๊ธฐ์ด ์ฝ๋
NestJS CLI
๋ฅผ ์ด์ฉํด NestJS Object๋ฅผ ์์ฑํ๊ฒ ๋๋ฉด ์๋์ผ๋ก ํ
์คํ
ํ์ผ์ธ .spec.ts
๊ฐ ์์ฑ๋๋ค.
์ด .sepc.ts
ํ์ผ์ Controller์ service๋ฅผ NestJS CLI
๋ก ์์ฑํ ๋์๋ง ์๋์ผ๋ก ์์ฑ๋๋ค.
์ฌ์ค ๋์ ์ฐจ์ด๋ ๊ฑฐ์ ์๋๋ฐ, ๋ง์ฝ ์ฐจ์ด๋ฅผ ๋ณด๊ณ ์ถ๋ค๋ฉด ํผ์ณ๋ณด๊ธฐ์ ๊ธฐ์ ์ ํด๋๊ฒ ๋ค.
controller.spec.ts vs. serivice.spec.ts
app.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CatController } from './cat.controller';
import { CatService } from './cat.service';
describe('CatController', () => {
let controller: CatController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CatController],
service: [CatService]
}).compile();
controller = module.get<CatController>(CatController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
app.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CatService } from './cat.service';
describe('CatService', () => {
let service: CatService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CatService],
}).compile();
service = module.get<CatService>(CatService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
์ ๋ง ์ฐจ์ด๊ฐ ์์ง ์์๊ฐ? ๐
์ฌ์ค ๋์ ์ฐจ์ด๊ฐ ๊ฑฐ์ ์๊ณ , ๋ค๋ฅธ ํฌ์คํธ๋ฅผ ์ฐพ์๋ด๋ ๋ณดํต Controller๋ฅผ ๊ธฐ์ค์ผ๋ก ์์ฑ๋์ด ์์ด์ ์ฌ๊ธฐ์์๋ controller๋ฅผ ๊ธฐ์ค์ผ๋ก controller.spec.ts
๋ฅผ ์์ฑํด๋ณด๊ฒ ๋ค.
NestJS
App์ ์์ฑํ ๋ ์๋์ผ๋ก ์์ฑ๋๋ app.controller.spec.ts
ํ์ผ์ด๋ค. ์ฐ์ ์ด ๋
์์ ๋ํด(ๅ่งฃ)ํด๋ณด์.
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});
jestjs
๋ ํ
์คํธ ์ผ์ด์ค๋ฅผ describe
๋ด๋ถ์ ์ ์ํ๋ค.
์์ ์ฝ๋์์๋ describe('AppController', ...)
์ ๋ด๋ถ์ describe('root', ...)
๊ฐ ์ ์๋์ด ์๋ค.
describe()
์ ์ธ์๋ก ๋ค์ด๊ฐ๋ String์ ๋จ์ํ Testing์ ์ ์ํ๋ ์ด๋ฆ์ ๋ถ๊ณผํ๋ค. ํ
์คํธ ๋ก์ง์๋ ์๋ฌด ๊ด๊ณ๊ฐ ์๋ค.
beforeEach()
์๋ ๊ฐ ํ
์คํธ ์ผ์ด์ค ์คํ ์ด์ ์ ์ ํํ ๋ด์ฉ์ด ์ ์๋์ด ์๋ค.
์์ ์ฝ๋๋ TestingModule
์ธ app
์ ์์ฑํ๋ค.
์ค์ ํ
์คํ
์ฝ๋๋ it()
์์ ์ ์๋๋ค. it()
์๋ String ์ธ์๊ฐ ๋ค์ด๊ฐ๋๋ฐ, describe()
์ ๊ทธ๊ฒ์ด ํ
์คํธ ์ผ์ด์ค์ ์ ์ํ๋ ์ด๋ฆ์ด๋ผ๋ฉด, it()
์ String์ ํ
์คํธ ์ผ์ด์ค์ ๋ํ ์ค๋ช
์ ์๋ฏธํ๋ค.
๊ทธ๋์ ์์ฝํ๋ฉด ์๋์ ๊ฐ๋ค!
describe('test title', () => {
it('test description', () => {
expect("value-wanting-to-test").tobe("value-wanting-to-get")
}
})
๋ฐ์ดํฐ๋ฒ ์ด์ค ํ ์คํธ
Jest
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์คํธ๋ฅผ ํ ์ ์๋ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ ์์๋ณด์!
MockRepository
์์ ์ํฉ์ ๋ฌธ์์ด ๋น๊ต ์์ค์ ๊ฐ๋จํ ํ ์คํ ์ด์ง๋ง, ์ค์ ์๋ฒ๋ฅผ ํ ์คํ ํ๊ธฐ ์ํด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๊ทผํ๋ API๋ค์ ํ ์คํธํด์ผ ํ๋ค!
ํ์ง๋ง, ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ง์ ์กฐ์ํ์ฌ ํ ์คํธ ํ๊ฒฝ์ ๋ง๋๋ ๊ฒ์ ์์ฃผ์์ฃผ ๋นํจ์จ์ ์ด๋ฉฐ, Unit Test์ ์์น๊ณผ๋ ๋ง์ง ์๋๋ค.
๊ทธ๋์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ง์ ์กฐ์ํ๋ ๊ฒ์ด ์๋๋ผ ํ
์คํธํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ชจ์ฌํ Mock
๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ํด๋น Mock
๊ฐ์ฒด์์ ํ
์คํธ ์ํฉ์ ๋ง๋ค์ด ํ
์คํธ๋ฅผ ์งํํด์ผ ํ๋ค!!
Mocking & Mock
ย โ์ด์ ํ๊ฒฝ ๋๋น ์ ์ฝ์ด ๋ง์ ํ ์คํธ ํ๊ฒฝ์์๋ ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๋ํ๊ฑฐ๋ ์ค์ ์ธ๋ถ API๋ฅผ ํธ์ถํ๊ธฐ๊ฐ ๋ถ๊ฐ๋ฅํ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ๊ฐ๋ น ๊ฐ๋ฅํ๋๋ผ๋, ์ด๋ ๊ฒ ์ธ๋ถ ์๋น์ค์ ์์กดํ๋ ํ ์คํธ๋ ํด๋น ์๋น์ค์ ๋ฌธ์ ๊ฐ ์์ ๊ฒฝ์ฐ ๊นจ์ง ์ ์์ผ๋ฉฐ ์คํ ์๋๋ ๋๋ฆด ์ ๋ฐ์ ์์ต๋๋ค.
ย ๋ฐ๋ผ์ ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ ๋ ์ธ๋ถ์ ์์กดํ๋ ๋ถ๋ถ์ ์์์ ๊ฐ์ง(Mock)๋ก ๋์ฒดํ๋ ๊ธฐ๋ฒ์ด ์์ฃผ ์ฌ์ฉ๋๋๋ฐ ์ด๋ฅผ ๋ชจํน(Mocking)์ด๋ผ๊ณ ํฉ๋๋ค. ๋ค์ ๋งํด, ๋ชจํน(Mocking)์ ์ธ๋ถ ์๋น์ค์ ์์กดํ์ง ์๊ณ ๋ ๋ฆฝ์ ์ผ๋ก ์คํ์ด ๊ฐ๋ฅํ ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ๊ธฐ ์ํด์ ์ฌ์ฉ๋๋ ํ ์คํ ๊ธฐ๋ฒ์ ๋๋ค.โ - article from here
ย โ์๋ฅผ ๋ค์ด, ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ ์ฝ๋์ ๋ํ ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ ๋, ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ์ฌ๋ฌ๊ฐ์ง ๋ฌธ์ ์ ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
- ๋ฐ์ดํ ๋ฒ ์ด์ค ์ ์๊ณผ ๊ฐ์ด Network์ด๋ I/O ์์ ์ด ํฌํจ๋ ํ ์คํธ๋ ์คํ ์๋๊ฐ ํ์ ํ ๋จ์ด์ง ์ ๋ฐ์ ์์ต๋๋ค.
- ํ๋ก์ ํธ์ ๊ท๋ชจ๊ฐ ์ผ์ ธ์ ํ ๋ฒ์ ์คํํด์ผ ํ ํ ์คํธ ์ผ์ด์ค๊ฐ ๋ง์ด์ง๋ฉด ์ด๋ฌํ ์์ ์๋ ์ ํ๋ค์ด ๋ชจ์ฌ ํฐ ์ด์๊ฐ ๋ ์ ์์ผ๋ฉฐ, CI/CD ํ์ดํ๋ผ์ธ์ ์ผ๋ถ๋ก ํ ์คํธ๊ฐ ์๋ํ๋์ด ์์ฃผ ์คํ๋์ผ ํ๋ค๋ฉด ๋ ํฐ ๋ฌธ์ ๊ฐ ๋ ์ ์์ต๋๋ค.
- ํ ์คํธ ์์ฒด๋ฅผ ์ํ ์ฝ๋๋ณด๋ค ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๊ฒฐ์ ๋งบ๊ณ ํธ๋์ญ์ ์ ์์ฑํ๊ณ ์ฟผ๋ฆฌ๋ฅผ ์ ์กํ๋ ์ฝ๋๊ฐ ๋ ๊ธธ์ด์ง ์ ์์ต๋๋ค. ์ฆ, ๋ฐฐ๋ณด๋ค ๋ฐฐ๊ผฝ์ด ๋ ์ปค์ง ์ ์์ต๋๋ค.
- ๋ง์ฝ ํ ์คํธ ์คํ ์๊ฐ ์ผ์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์คํ๋ผ์ธ ์์ ์ค์ด์๋ค๋ฉด ํด๋น ํ ์คํธ๋ ์คํจํ๊ฒ ๋ฉ๋๋ค. ๋ฐ๋ผ์ ํ ์คํธ๊ฐ ์ธํ๋ผ ํ๊ฒฝ์ ์ํฅ์ ๋ฐ๊ฒ๋ฉ๋๋ค. (non-deterministic)
- ํ ์คํธ๊ฐ ์ข ๋ฃ ์ง ํ, ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ณ๊ฒฝ ๋ฐ์ดํฐ๋ฅผ ์ง์ ์๋ณตํ๊ฑฐ๋ ํธ๋ ์ญ์ ์ rollback ํด์ค์ผ ํ๋๋ฐ ์๋นํ ๋ฒ๊ฑฐ๋ก์ด ์์ ์ด ๋ ์ ์์ต๋๋ค.
๋ฌด์๋ณด๋ค ์ด๋ฐ ๋ฐฉ์์ผ๋ก ํ ์คํธ๋ฅผ ์์ฑํ๊ฒ ๋๋ฉด ํน์ ๊ธฐ๋ฅ๋ง ๋ถ๋ฆฌํด์ ํ ์คํธํ๊ฒ ๋ค๋ ๋จ์ ํ ์คํธ(Unit Test)์ ๊ทผ๋ณธ์ ์ธ ์ฌ์์ ๋ถํฉํ์ง ์๊ฒ ๋ฉ๋๋ค.โ - article from here
๊ทธ๋์ ์๋ฒ์ ์ ๋ ํ
์คํธ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ง์ ์กฐ์ํ๋ ๊ฒ์ด ์๋ ์๋ฒ๋ฅผ ํ๋ด๋ด๋ MockRepository
๋ฅผ ๋ง๋ค์ด ์งํํ๋ค.
๋ณธ๋ service์์ Repository ๋ณ์๋ฅผ ๋ง๋ค์ด ํด๋น Repository๋ฅผ ์ด์ฉํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๊ทผํ๋ค. ํ ์คํธ์์๋ ์๋์ ๊ฐ์ด Repository๋ฅผ ๋ชจ์ฌํ โMockRepositoryโ๋ฅผ ๋ง๋ ๋ค.
์๋ฅผ ๋ค์ด UserRepository๋ฅผ ๋ชจ์ฌํ MockRepository
๋ฅผ ๋ง๋ค์ด๋ณด์.
class MockRepository {
async findOneOrFail(query) {
const user: User = new User();
user.uuid = query.uuid;
return user;
}
}
describe('User', () => {
let userService: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: getRepositoryToken(User),
useClass: MockRepository,
},
],
}).compile();
userService = module.get<UserService>(UserService);
});
it('should', async () => {
const userId = '42';
const result = await userService.findUserById(userId);
expect(result.uuid).toBe(userId);
});
});
Repository๋ฅผ ๋ชจ์ฌํ MockRepository
๋ฅผ ์ด์ฉํ๊ธฐ ์ํด์๋ Repository์ ์ ์๋ ํจ์๋ค์ ์ ์ธํ๊ณ ๋ชจ์ฌํด์ค์ผ ํ๋ค. ๋ง์ฝ ํ
์คํธ ํ๋ ค๋ service
์ ํน์ ํจ์, ์๋ฅผ ๋ค๋ฉด findUserById
๊ฐ์ ํจ์๊ฐ ๋ด๋ถ์์ Repository์ repository.findOne()
๊ณผ ๊ฐ์ ํจ์๋ฅผ ์ฌ์ฉํ๋ค๋ฉด, MockRepository
์์ ํด๋น ํจ์๋ฅผ ์ ์ธํด์ค์ผ ํ๋ค๋ ๋ง์ด๋ค!
Jest: spyOn
์ฐธ๊ณ ์๋ฃ: link
1. jest.fn()
; Mock function
jest.fn()
์ ์ด์ฉํด Mock function์ ์์ฑํ ์ ์๋ค.
const mockFn = jest.fn();
Mock function์ ์ผ๋ฐ ํจ์์๋ ๋ฌ๋ฆฌ ๋ชจํน์ ์ด์ฉํ ํ ์คํธ์ ํนํํ ํจ์๋ฅผ ๋ชจ์ฌํ โ๊ฐ์ฒดโ์ ๋๋ค.
* ์ธ์ ์ ๋ ฅ
mockFn()
mockFn(1)
mockFn("Lorem")
mockFn({ name: "Lorem", id: "Ipsum" })
* ๋ฆฌํด ๊ฐ ์ค์
mockFn.mockReturnValue("Lorem Ipsum");
console.log(mockFn()); // "Lorem Ipsum"
* Mock ๋น๋๊ธฐ ํจ์
mockFn.mockResolvedValue("Async resolve value");
mockFn.then((result) => {
console.log(result); // "Async resolved value"
})
* Mock function ๊ตฌํ
mockFn.mockImplementation((name) => `I am ${name}!`);
console.log(mockFn("Cantor")); // "I am Cantor!"
Mock function์ ์ ์ฉ์ฑ์ Mock function์ ํธ์ถ์ ๋ํ ์ ๋ณด๋ฅผ ๋ชจ๋ ๊ธฐ์ตํ๊ณ ์๋ค๋ ์ ์ด๋ค!!
mockFn("a")
mockFn(["b", "c"])
expect(mockFn).toBeCalledTimes(2)
expect(mockFn).toBeCalledWith("a")
expect(mockFn).toBeCalledWith(["b", "c"])
2. jest.spyOn()
; Spy Function
์ง๊ธ๊น์ง๋ ๋ชจ๋ ๊ธฐ์กด ๊ฐ์ฒด์ ๋์ ํ๋ โ๋์ญ(ไปฃๅฝน)โ์ธ Mock์ ์ด์ฉํ ํ ์คํธ๋ฅผ ์ดํด๋ดค๋ค. ํ์ง๋ง ๋ช๋ช ๊ฒฝ์ฐ์๋ ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ Mock๋ก ๋์ฒดํ์ง ์ด๋ ค์ธ ์๋ ์๋ค. ์ด ๊ฒฝ์ฐ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ฐ๋ก โSpy Functionโ์ด๋ค!
์๋ฅผ ๋ค์ด ์๋์ ๊ฐ์ด calculator
์ ์ ์๋ add
์ Spy Function์ ๋ง๋ค์ด ์ฌ์ฉํ ์ ์๋ค.
const calculator = {
add: (a, b) => a + b,
}
const spyFn = jest.spyOn(calculator, "add")
const result = calculator.add(2, 3)
expect(spyFn).toBeCalledTimes(1)
expect(spyFn).toBeCalledWith(2, 3)
expect(result).toBe(5)
์์์ Spy function์ Mock ํ ์ ์์ ๋ ์ฌ์ฉํ๋ค๊ณ ํ๋ค. Mock ํ ์ ์๋ ๊ฒฝ์ฐ๋, โํ ์คํ ๋์ ํจ์ A๊ฐ ๋ค๋ฅธ ํจ์ B ๋ด๋ถ์์ ํธ์ถ๋๋ฉฐ ์ฌ์ฉ๋๋ ์ํฉ์ด๋ผ ํจ์ A๋ฅผ mockingํ ๊ฒฝ์ฐ, ํจ์ B๋ฅผ ํ ์คํธํ ์ ์๊ธฐ ๋๋ฌธ์ ์๋ณธ์ ๊ทธ๋๋ก ๋๊ณ Spy ํ๋ค.โ๋ผ๊ณ ํ๋ค.
> Mock vs. Spy link
์ด๊ณณ์ ๊ฒ์๋ .spyon()
ํจ์์ ์์๋ฅผ ์ดํด๋ณด์.
describe('UserService', () => {
describe('์ ์ ์ ๋ณด ์์ ', () => {
it('์กด์ฌํ์ง ์๋ ์ ์ ์ ๋ณด๋ฅผ ์์ ํ ๊ฒฝ์ฐ BadRequestError ๋ฐ์ํ๋ค.', async () => {
const userId = faker.random.uuid();
const updateUserDto: UpdateUserDto = {
firstName: faker.lorem.sentence(),
lastName: faker.lorem.sentence(),
isActive: false,
};
const userRepositoryFindOneSpy = jest
.spyOn(userRepository, 'findOne')
.mockResolvedValue(null);
try {
await userService.updateUser(userId, updateUserDto);
} catch (e) {
expect(e).toBeInstanceOf(BadRequestException);
expect(e.message).toBe(Message.NOT_FOUND_USER_ITEM);
}
expect(userRepositoryFindOneSpy).toHaveBeenCalledWith({
where: {
id: userId,
},
});
});
});
})
์ฌ๊ธฐ์๋ .spyOn()
์ผ๋ก repository์ ํจ์๋ฅผ ๋ชจ์ฌํ๋๋ผ๋ ํจ์์ ๋ก์ง์ ์์ ํ๋ ์์
์ด ํ์ํ๋ค!!
๋ํ .spyOn()
์ ๋ฆฌํด์ผ๋ก ์ป์ Spy Function ๊ฐ์ฒด๋ .toHaveBeenCalled...()
ํจ์ ๋ฑ์ผ๋ก ํ
์คํ
ํ ํจ์ ๋ด๋ถ์์ ๋ชจ์ฌํ ํจ์๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ๋์ง๋ฅผ ๊ฒ์ฆํ๋ ๋ฐ์ ์ฌ์ฉ๋๋ค. ์ฐ๋ฆฌ๊ฐ ๊ฒ์ฆํ ๋์์ .spyOn()
์ผ๋ก Mockingํ ๋์์ด ์๋๋ค. .spyOn()
์ ํฌํจํ Mocking์ ๋จ์ง ์์กด์ฑ์ ๋๊ธฐ ์ํ ์๋จ์ผ ๋ฟ์ด๋ค!!
๊ฒฐ๊ตญ .spyOn()
์ ์ฌ์ฉํ๋๋ผ๋ ๊ฒฐ๊ตญ์ ๊ธฐ์กด Mocking๊ณผ ๋น์ทํ ๋งฅ๋ฝ์ผ๋ก ํ
์คํ
์ด ์งํ๋๋ค๋ ๊ฒ์ด๋ค! ์กฐ์ผ๋ชจ์ฌ