Jest(4) - Mock 과 Stub 차이

매튜·2023년 11월 1일
1

우아한테크코스

목록 보기
5/5
post-thumbnail

아래 코드는, 우리가 이전에 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 vs Stub

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 }]);
  });
});
profile
안녕하세요, 개발자 매튜 / 김용민입니다. 현재는 타입스크립트를 활용하여, 프론트엔드와 백엔드를 개발하고 있습니다! 새로운 것을 배우고 실전에 적용시키는 것을 좋아합니다!

0개의 댓글