Jest(3) - Jest Mock 이용 테스트 (의존성 부시기)

매튜·2023년 11월 1일
0

우아한테크코스

목록 보기
4/5
post-thumbnail

Mock을 이용해서, 제품 정보를 가지고 오는 테스트를 해볼려고 합니다.

실제로 두개의 클래스로 이루어진 파일이 있습니다.

class ProductClient {
  fetchItems() {
    return fetch('http://example.com/login/id+password').then((response) =>
      response.json()
    );
  }
}

module.exports = ProductClient;

ProductClient 클래스는, 가상의 주소로 데이터 요청을 받는 코드입니다. 실제로 네트워크 동작은 하지 않습니다.

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;

ProductService에서 ProductClient를 사용한다면, 서로 모듈간의 상호작용을 테스트를 하면 안되고, 단일의 테스트만 진행해야합니다.

ProductService 내부에서, ProductClient를 사용하기 떄문에, this.productClientfetchItems라는 함수도 우리도 모르게 내부적으로 테스트가 됩니다.

그래서 클라이언트 내부에서 함수가 실패하거나 데이터를 받아오지 못해서 또는 일시적으로 네트워크에 문제가 생긴다면, 서비스 테스트 코드는 실패하게 됩니다.

네트워크 상태에 의존하는 테스트 코드는 좋지 않습니다.

따라서, ProductClient와 별개로 독립적으로 의존하지않도록 ProductClient 자체를 mock 하면 됩니다.

ProductService 내부에서 Product Client를 사용하기에, jest에서 제공하는 mock을 사용할 수 있습니다.

const ProductService = require('../product_service_no_di.js');
const ProductClient = require('../product_client.js');

// 가품의 프로덕트 클라이언트를 쓴다고 명시
// 실제로 구현된 코드를 사용하는 것이 아닌, 가품의 productClient를 사용한다고 명시!
jest.mock('../product_client');

describe('ProductService', () => {
	// 실제로, productClient에서 데이터 요청을 하는데, 어떤 데이터를 받는지 우리가, 리턴값을 가상으로 정의
	// fetchItems를 호출하면 mock 함수가, 비동기적으로 아래의 배열을 호출할것입니다.

	const fetchItems = jest.fn(async () => [
		{ item: 'Milk', available: true },
		{ item: 'Banana', available: false },
	])
	
		// 다음은 가상의 fetchItems 함수와,mock하고있는 productClient를 연결해주면 됩니다.
	ProductClient.mockImplementation(() => {
		return {
			fetchItems, // 생략 fetchItems: fetchItems;
		}
	});
	let productService;

	beforeEach(() => {
		productService = new ProductService();
	});

});

위의 코드처럼 작성하면, ProductService의 클래스를 테스트하는 것 입니다.

이 곳에 **어떤 다른 모듈 클래스를 사용하든, 영향을 받지 않도록** 나머지 **다른 의존성에 대해서는 위의 코드처럼 mock을 이용**하면됩니다.

**mock 함수와, mockImplementation을 통해서 어떤 함수를 호출했을 때 어떤 결과를 도출하는지 연결**해주는 작업을 한 겁니다.

그러면, 우리는 ProductService 클래스의 **available이 true인 애들만 필터링 하는 작업에만 집중**해서 테스트를 작성할 수 있습니다.

이렇게 mock을 이용했을 떄 큰 장점은 productClient가 실패하든, 네트워크 자체에서 실패를하든 환경적인 요인에 영향을 받지 않습니다. 우리가 원하는 로직만 날카롭게 검증할 수 있습니다. 이것이 **단위 테스트**입니다.

우리는, 이제 ProductService의 기능만 테스트를 진행하면 됩니다.

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();
  });

  it('사용 가능한(available) 아이템만 필터링 하는 테스트', async () => {
    const items = await productService.fetchAvailableItems();
    expect(items.length).toBe(1);
    expect(items).toEqual([{ item: 'Milk', available: true }]);
  });
	// * mock을 이용할 떄 주의할 점, 위의 테스트 말고도, 여러개의 테스트를 진행한다고 가정.
  it('test', async () => {
    const items = await productService.fetchAvailableItems();
		// 클라이언트에서는, fetchItems라는 mock함수가 한번만 호출되어야 함.
    expect(fetchItems).toHaveBeenCalledTimes(1);
  });
});

추가 케이스를 추가해서, 테스트를 실행하면 모두 성공하는 것을 확인할 수 있습니다.

// jest.config.js
module.exports = {
	clearMocks: true,
}

별도로, 다른 것들을 하지 않아도 각각의 테스트가 수행이 되면 자동으로 Mock에 관한게 초기화 되기 떄문입니다.

// jest.config.js
// false로 바꾸면 실패한 것을 확인할 수 있다.
module.exports = {
	clearMocks: false,
}

왜 실패했는지, 살펴보면 총 2개의 테스트 케이스를 지금 테스트 하고 있는데

첫번째 테스트 케이스에서도 결국에는 fetchItems 라는 함수를 호출했습니다.

두번쨰 테스트 케이스에서도 동일하게 fetchItems 라는 함수를 호출했기 때문입니다.

총 2번 호출했음을 확인할 수 있습니다.

clearMocks를 false라고 했을 때 우리가 이러한 에러를 방지하기 위해서는 beforEach 문에, 항상 테스트가 실행되기 전에 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();
		// clearMocks를 false로 설정했을 때 항상, 코드 실행 전에 mockClear()를 해주어야 한다.
    fetchItems.mockClear();
    ProductClient.mockClear();
  });

  it('사용 가능한(available) 아이템만 필터링 하는 테스트', 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);
  });
});

beforeEach문에서, clearMocks를 false로 설정했을 때 항상, 코드 실행 전에 mockClear()를 해주어야 한다.

완성 코드

const ProductService = require('../product_service_no_di.js');
const ProductClient = require('../product_client.js');

// 가품의 프로덕트 클라이언트를 쓴다고 명시
jest.mock('../product_client');

// Mock 을 남용하는 좋지 않은 사례임!

// {return} 은생략가능
describe('ProductService', () => {
  // ProductService Class를 테스트하는건데, ProductService에서 어떤 다른 모듈 클래스를 사용하든, 그것들에 영향을 받지 않도록, 나머지 모든 의존성에 대해서는 mock을 이용합니다.
  // mock 함수를 선언하고
  const fetchItems = jest.fn(async () => {
    return [
      { item: 'Milk', available: true },
      { item: 'Banana', available: false },
    ];
  });
  // mockImplementation을 통해, 어떤 함수를 호출했을 떄 어떤 데이터를 갖고오는지 컨트롤할 수 있게 만들었습니다.
  ProductClient.mockImplementation(() => {
    return {
      fetchItems: 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);
  });
});

// 이렇게 의존성을 낮추면 ProductClient 실제 데이터 요청을 하는 곳에서 네트워크 요청이
// 실패하든 말든, 환경적인 요인에 영향을 받지 않습니다. 원하는 로직만 날카롭게 검증
// 이것이 단위 테스트이다.
// 서로간의 의존성이 있다면, 목을 이용하여 의존성이 없도록 만들 수 있다.
profile
안녕하세요, 개발자 매튜 / 김용민입니다. 현재는 타입스크립트를 활용하여, 프론트엔드와 백엔드를 개발하고 있습니다! 새로운 것을 배우고 실전에 적용시키는 것을 좋아합니다!

0개의 댓글