NestJS: Mocking & Spy
λ³Έ κΈμ μ κ° NestJS νλ μμν¬λ₯Ό ν΅ν΄ κ°λ°νλ©΄μ κΉ¨λ¬μ λ
Ένμ°λ₯Ό κΈ°λ‘ν κ²μ
λλ€. μ κ° μ μν λ°©λ²λ³΄λ€ λ μ’μ λ°©λ²μ΄ μμ μλ μμ΅λλ€. μ§μ μ μΈμ λ νμμ
λλ€ :)
NestJSμμ Javascriptμ ν
μ€νΈ νλ μμν¬μΈ jestlinkλ₯Ό κΈ°λ³ΈμΌλ‘ νλ ν
μ€νΈ νλ μμν¬λ₯Ό μ§μνλ€.
λ¬Όλ‘ μ½κ°μ λ³νμ μκ² μ§λ§, κ·Έλ₯ μ§μλ§ νλ μμ€μ΄ μλλΌ 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κ³Ό λΉμ·ν λ§₯λ½μΌλ‘ ν
μ€ν
μ΄ μ§νλλ€λ κ²μ΄λ€! μ‘°μΌλͺ¨μ¬