아래 코드는, 우리가 이전에 Jest Mock을 이용하여, 의존성을 부시는 방법 즉, 단위 테스트를 제대로 하는 방법에 대한 코드를 작성하였습니다.
const ProductService = require('../product_service_no_di.js');
const ProductClient = require('../product_client.js');
jest.mock('../product_client');
describe('ProductService', () => {
const fetchItems = jest.fn(async () => {
return [
{ item: 'Milk', available: true },
{ item: 'Banana', available: false },
];
});
ProductClient.mockImplementation(() => {
return {
fetchItems: fetchItems,
};
});
let productService;
beforeEach(() => {
productService = new ProductService();
fetchItems.mockClear();
ProductClient.mockClear();
});
it('should filter out only available items', async () => {
const items = await productService.fetchAvailableItems();
expect(items.length).toBe(1);
expect(items).toEqual([{ item: 'Milk', available: true }]);
});
it('test', async () => {
const items = await productService.fetchAvailableItems();
expect(fetchItems).toHaveBeenCalledTimes(1);
});
});
Mock은, 구현 사항이 없고 내가 원하는 것만 부분적으로 가짜의 흉내를 냅니다.
Stub은 진짜의 interface를 충족하는, 실제로 구현된 코드인데 진짜 코드와 대체 가능한 것을 의미합니다.
아래 코드에는 의존성 관련하여 치명적인 문제점이 있습니다.
const ProductClient = require('./product_client');
class ProductService {
constructor() {
this.productClient = new ProductClient();
}
fetchAvailableItems() {
return this.productClient
.fetchItems()
.then((items) => items.filter((item) => item.available));
}
}
module.exports = ProductService;
클래스 내부에서, 스스로의 의존을 결정하고, 정의하고 만들어서 사용하는 것은 **dependency injection의 원칙에 어긋**
납니다. 필요한 것은 내부적으로 만들어서 사용하는 것이 아닌 외부에서 받아와야 합니다.
즉, 위의 코드와 다르게 아래 코드는 ProductClient
를 받아올 필요가 없습니다.
fetchItems()
함수를 가진 productClient를 외부로 부터 주어진 것을 활용
하면 됩니다. 이를 우리는 의존성을 역전시킨다 라고 합니다. 외부로부터 주어진 것을 사용하면서, 이제 테스트 할 떄는 테스트용 ProductClient를 주입
하면 됩니다. 실제로 Production에서는 실제의 ProductClient를 주입
하면 됩니다.
아래 코드가, 의존성을 역전시킨 ProductService 클래스입니다. (의존성 문제 해결)
class ProductService {
constructor(productClient) {
this.productClient = productClient;
}
fetchAvailableItems() {
return this.productClient
.fetchItems()
.then((items) => items.filter((item) => item.available));
}
}
module.exports = ProductService;
이제, 테스트를 할 떄는 Stub
을 활용하면 됩니다. 실제 mock
을 받아오지 않으므로 이전 테스트 코드보다 조금 더 깔끔해짐을 확인할 수 있다.
const ProductService = require('../product_service_no_di.js');
// 실제 클래스를 받아오는 것이 아닌, 가상의 Stub 클래스를 받아와서 작업을 진행한다.
// const ProductClient = require('../product_client.js');
const StubProductClient = require('./stub_product_client.js');
describe('ProductService - Stub', () => {
let productService;
beforeEach(() => {
productService = new ProductService(new StubProductClient());
});
// 실제 mock을 받아오지 않으므로 테스트 코드가 조금 더 깔끔해짐을 확인할 수 있다.
it('should filter out only available items', async () => {
const items = await productService.fetchAvailableItems();
expect(items.length).toBe(1);
expect(items).toEqual([{ item: 'Milk', available: true }]);
});
});