nestjs 프로젝트를 진행하며 테스트를 진행했습니다. 이때,레포지토리를 변수로 직접 모킹하였습니다.
// espense.service.spec.ts
const mockUserRepository = {
findOne: jest.fn(),
}
const mockBudgetRepository = {
createQueryBuilder: jest.fn(),
}
const mockExpenseRepository = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(),
}
const mockCategoryRepository = {
findOne: jest.fn(),
}
describe('ExpenseService', () => {
let service: ExpenseService
let userRepository: UserRepository
let budgetRepository: Repository<Budget>
let expenseRepository: Repository<Expense>
let categoryRepository: Repository<Category>
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ExpenseService,
{
provide: UserRepository,
useValue: mockUserRepository,
},
{
provide: getRepositoryToken(Budget),
useValue: mockBudgetRepository,
},
{
provide: getRepositoryToken(Expense),
useValue: mockExpenseRepository,
},
{
provide: getRepositoryToken(Category),
useValue: mockCategoryRepository,
},
],
}).compile()
service = module.get<ExpenseService>(ExpenseService)
userRepository = module.get<UserRepository>(UserRepository)
budgetRepository = module.get<Repository<Budget>>(
getRepositoryToken(Budget),
)
expenseRepository = module.get<Repository<Expense>>(
getRepositoryToken(Expense),
)
categoryRepository = module.get<Repository<Category>>(
getRepositoryToken(Category),
)
})
테스트를 작성하면서 mockExpseRepository를 변수를 활용해 직접 모킹하는 방법, jest.spyOn을 사용하여 모킹하는 방법이 있다는 것을 알게 되었습니다. 이 두 방법에 대해 차이점이 무엇인지 알아보겠습니다.
mockExpenseRepository.create.mockReturnValue(expectedExpense);
mockExpenseRepository.save.mockResolvedValue(expectedExpense);
jest.spyOn(expenseRepository, 'create').mockReturnValue(expectedExpense)
jest.spyOn(expenseRepository, 'save').mockResolvedValue(expectedExpense)
mockReturnValue
/ mockResolvedValue
사용)mockExpenseRepository.create.mockReturnValue(expectedExpense);
mockExpenseRepository.save.mockResolvedValue(expectedExpense);
mockExpenseRepository
객체를 생성하고 해당 객체의 메소드를 직접 오버라이드합니다. 이 경우 create
메소드는 동기적으로 expectedExpense
를 반환하고, save
메소드는 비동기적으로 expectedExpense
를 반환하도록 설정됩니다.expenseRepository
의 인스턴스를 사용하지 않고, 대신 테스트를 위해 제공된 mockExpenseRepository
인스턴스를 사용한다고 가정할 때 적합합니다.jest.spyOn
사용jest.spyOn(expenseRepository, 'create').mockReturnValue(expectedExpense);
jest.spyOn(expenseRepository, 'save').mockResolvedValue(expectedExpense);
jest.spyOn
방식은 실제 expenseRepository
객체의 create
및 save
메소드를 감시(spy)하고, 이 메소드들이 호출될 때마다 지정된 동작을 수행하도록 설정합니다. 이 경우에도 create
는 동기적으로, save
는 비동기적으로 expectedExpense
를 반환하도록 모킹됩니다.jest.spyOn
은 실제 객체의 메소드 호출을 감시하고 필요에 따라 그 동작을 재정의합니다.jest.spyOn
을 사용하면 원본 메소드의 호출 여부와 인자 등을 검증할 수 있으며, 테스트가 끝난 후 mockRestore
를 호출하여 원래 상태로 복원할 수 있는 옵션이 있어 테스트 간의 상태 격리를 용이하게 합니다.mock과 stub은 테스트 환경에서 사용되는 가짜 객체를 말한다.
Mock 객체는 테스트 대상의 외부 의존성을 가짜로 만든 객체입니다. 테스트 중에 해당 객체가 어떻게
사용되는지 검증하는 데 사용합니다. 테스트 대상이 Mock 객체와 상호작용이 올바른지 검증합니다. 호출된 메서드, 전달된 매개변수, 호출 횟수 등을 검사하여 테스트 대상이 예상대로 외부와 상호작용하는지 확인할 수 있습니다.
Mock에 대한 예시 코드입니다.
주문 상태를 업테이트 하는 서비스(OrderService)가 결제 시스템(PaymentService)을 의존한다고 가정합니다.
먼저, 결제와 관련된 로직을 처리하는 서비스의 인터페이스를 정의합니다.
// payment.service.ts
export interface PaymentService {
validatePayment(orderId: string): Promise<boolean>;
}
PaymentService를 사용하여 주문의 결제 상태를 업데이트하는 서비스입니다.
// order.service.ts
import { Injectable } from '@nestjs/common';
import { PaymentService } from './payment.service';
@Injectable()
export class OrderService {
constructor(private paymentService: PaymentService) {}
async updateOrderStatus(orderId: string): Promise<string> {
const isValid = await this.paymentService.validatePayment(orderId);
return isValid ? '결제 확인됨' : '결제 실패';
}
}
위 서비스 코드에 대해 Jest를 사용해서 OrderService의 테스트 코드를 작성합니다. 이 과정에서 PaymentService의 Mock 객체를 생성하고 validatePayment 메서드가 특정한 값을 반환하도록 설정합니다.
// order.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { OrderService } from './order.service';
import { PaymentService } from './payment.service';
describe('OrderService', () => {
let service: OrderService;
let paymentServiceMock: PaymentService;
beforeEach(async () => {
// PaymentService의 목 객체 생성
const paymentServiceMockProvider = {
provide: PaymentService,
useFactory: () => ({
validatePayment: jest.fn((orderId: string) => Promise.resolve(true)), // 항상 true를 반환하도록 설정
}),
};
const module: TestingModule = await Test.createTestingModule({
providers: [OrderService, paymentServiceMockProvider],
}).compile();
service = module.get<OrderService>(OrderService);
paymentServiceMock = module.get<PaymentService>(PaymentService);
});
it('should return "결제 확인됨" when payment is valid', async () => {
// 목 객체의 메서드가 예상대로 호출되었는지 검증
expect(await service.updateOrderStatus('123')).toBe('결제 확인됨');
expect(paymentServiceMock.validatePayment).toHaveBeenCalledWith('123');
});
});
jest.fn()
을 사용해서 PaymentService의 validatePayment 메서드를 Mock으로 사용했습니다. 이 Mock 메서드는 모든 orderId에 대해 true로 반환하도록 설정되어 있습니다. 테스트는 OrderService가 PaymentService.validatePayment 메서드가 호출되고, 예상된 인자를 받는지 확인하고 '결제 확인됨'을 반환하는지 확인합니다.
Stub이란 테스트 중인 모듈이 호출하는 다른 소프트웨어 구성요소(예: 모듈, 변수, 객체)를 일시적으로 대체하는 소프트웨어 구성요소를 말합니다. Stub을 사용하는 경우는 아래와 같습니다.
- 호출하는 함수가 아직 구현되지 않았을 때
- 함수가 반환하는 값을 임의로 생성
- 복잡한 논리 흐름을 가지는 경우 테스트를 단순화할 목적으로 사용
Stub에 대한 예를 들어보겠습니다. 온라인 쇼핑몰에서 사용자의 주문 내역을 가져오는 기능을 개발한다고 가정합니다. 이 기능은 외부 결제 시스템 API를 호출하여 사용자가 결제한 주문의 상태를 확인해야합니다. 단위 테스트를 진행할 때, 실제 결제 시스템이 아직 구현되지 않았다고 가정하고 Stub 객체를 사용하여 테스트합니다.
Stub를 사용하기 위해서는 결제 시스템 API 호출 부분을 객체로 대체합니다. 이 Stub 객체는 주문 ID '1234'가 입력되었을 때, '결제 완료'라는 고정된 응답을 반환하도록 구현됩니다.
먼저, 외부결제 시스템과 통신하기 위해 클라이언트의 인터페이스를 정의합니다.
// payment.client.ts
export interface PaymentClient {
getOrderStatus(orderId: string): Promise<string>;
}
// payment.client.ts
import { Injectable } from '@nestjs/common';
import { PaymentClient } from './payment.client';
@Injectable()
export class PaymentClientImpl implements PaymentClient {
async getOrderStatus(orderId: string): Promise<string> {
// 실제 외부 API 호출 로직이 구현안된
return '결제 완료'; // 예시이므로 간단한 반환값으로 대체
}
}
테스트에 사용할 PayClient를 stub로 구현했습니다.
// payment.client.stub.ts
import { PaymentClient } from './payment.client';
export class PaymentClientStub implements PaymentClient {
async getOrderStatus(orderId: string): Promise<string> {
if (orderId === '1234') {
return '결제 완료';
}
return '대기 중';
}
}
PaymentClient를 사용하여 주문 상태를 확인하는 서비스입니다.
// payment.client.stub.ts
import { PaymentClient } from './payment.client';
export class PaymentClientStub implements PaymentClient {
async getOrderStatus(orderId: string): Promise<string> {
if (orderId === '1234') {
return '결제 완료';
}
return '대기 중';
}
}
// order.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { OrderService } from './order.service';
import { PaymentClient } from './payment.client';
import { PaymentClientStub } from './payment.client.stub';
describe('OrderService', () => {
let service: OrderService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
// OrderService에 PaymentClientStub을 주입
providers: [
OrderService,
{
provide: PaymentClient,
useClass: PaymentClientStub,
},
],
}).compile();
service = module.get<OrderService>(OrderService);
});
it('should return "결제 완료" for orderId 1234', async () => {
expect(await service.checkOrderStatus('1234')).toBe('결제 완료');
});
});
위 코드에서 PaymentClientStub은 테스트 중에 OrderService가 PaymentClient의 실제 구현 대신 사용하는 Stub입니다. 이 Stub는 PaymentClient 인터페이스를 구현하며, getOrderStatus 메서드에서 주어진 조건에 따라 고정된 응답을 반환합니다. 이를 통해 외부 API의 실제 통신 없이 OrderService의 로직을 테스트할 수 있습니다.