[2] 단위 테스트 (Unit Test) - 단위 테스트의 논리 구성

김법우·2022년 8월 2일
0

Nest.js

목록 보기
7/10
post-thumbnail

Jest 를 사용한 단위 테스트 코드 작성

Mocking

왜 Mocking 해야 하는가?

우리는 이전에 단위 테스트가 지켜야 하는 5가지 규칙, FIRST 에 대해 알아보았다. 좋은 단위 테스트를 작성하기 위해서는 해당 규칙을 지키는 것이 바람직하다.

처음으로 테스트 코드를 작성하기 위해 코드창을 열때 느껴지는 감정은 막막함이다. service class 는 여러개의 repository, 다른 모듈의 service class, plugin 모듈의 service class 등등 다양한 외부 클래스를 주입 받아 사용하고 있을 것이다.

이전 글에서도 언급했지만, 테스트 코드의 목적은 기능이 구현해야 할 의도를 잘 나타내는 것에 있다. 실제로 service class 가 수행하는 데이터 베이스 접속, I/O 작업을 단위 테스트에서도 똑같이 구현하는 것은 대부분의 경우에서 바람직하지 않다.

단위 테스트는 빠르고, 반복 가능하며 독립적이어야 하는데 (이미 코드를 쓰고 테스트 코드를 쓰는 순간에서 Timely 규칙은 어기게 된다 …) 테스트용 데이터베이스라 할 지라도 데이터베이스를 연결하는 등의 작업을 구현하는 순간 느리고, 반복 가능하지 않고, 독립적이지 않은 테스트 코드가 된다.

그렇기 때문에 우리는 Mocking 을 통해 “의도된 바를 확인 할 수 있도록" 에 중심을 두고 외적인 부분들을 가짜 객체로 대채하는 과정을 거쳐야 한다.

Mocking 이란?

Mocking 이란 특정 소스 코드가 실행 되기 위해 의존하는 부분을 모의 객체로 대체하는 기법을 말한다. 간단히 예시를 들자면

findAll(user_id: number) {
  const userInfo:UserInfo = await this.userService.findOne(user_id);
  const postingList:Posting[] = await this.postingRepository.find({
    where: {
      user: userInfo,
    },
  });

  // 기타 다른 로직들
}

위와 같이 유저 정보를 조회하고, 유저 정보를 토대로 해당 유저의 모든 포스팅을 조회하는 method 에 대해 단위 테스트를 작성해야한다고 가정해보자.

우리가 위의 코드에서 의도하는 바는 “유저 정보를 조회하고” + “유저 정보로 포스팅 목록을 조회한다" 이다. 이것을 올바르게 검증하기 위해서는 어떤 부분을 확인해야 할까?

typeORM 의 findOne, find 가 주어진 조건에 따라 데이터를 잘 조회하는지를 검증하는 것이 아니라 “userService.findOne 을 입력 받은 user_id 로 1번 조회해 UserInfo 객체를 userInfo 변수에 할당”하고,

“postingRepository.find 을 위에서 조회한 userInfo 로 1번 조회해 Posting[ ] 객체 배열을 postingList 에 할당”하는 것을 확인해야 하는 것이다.

const mainSpyFn = jest.spyOn(service, 'findAll');

**// [1-1] userService.findOne 은 모의 데이터를 반환한다.**
const userServiceFindOneSpyFn = jest.spyOn(userService, 'findOne').mockResolvedValue(
  // ... 모의 데이터
);

**// [1-2] postRepository.find 는 모의 데이터를 반환한다.**
const postRepoFind = jest.spyOn(postRepository, 'find').mockResolvedValue(
  // ... 모의 데이터
);

const result = await service.findAll(user_id);

expect(result).toStrictEqual(null);
expect(mainSpyFn).toBeCalledTimes(1);
expect(mainSpyFn).toBeCalledWith(user_id);

**// [2-1] userService.findOne 은 1회, user_id 를 통해 호출되어야 한다.**
expect(userServiceFindOneSpyFn).toBeCalledTimes(1);
expect(userServiceFindOneSpyFn).toBeCalledWith(user_id);

**// [2-2] postRepository.find 는 1회, userInfo 를 통해 호출되어야 한다.**
expect(postRepoFind).toBeCalledTimes(1);
expect(postRepoFind).toBeCalledWith({
  where: {
    user: userInfo
  }
});

[1] 번 부분은 boardService.findAll 이 의존하는 2가지 함수 userService.findOne, postRespository.find 의 실행 결과를 모의 객체로 대체하면서 해당 함수의 호출 여부와 호출 방식을 알아 낼 수 있도록 하는 부분이다.

[2] 번 부분은 우리가 의도한 바인 “userService.findOne 의 user_id 를 통한 1회 호출” 을 검증하고, “ postRepository.find 를 userService.findOne 의 결과로 1회 호출"을 검증 할 수 있도록, 함수 구현 자체를 가짜로 대체하지 않고 결과값만을 가짜로 대체하는 것이다.

이번 포스팅에서는 구체적인 단위 테스트 구현 방법은 조금 뒤로 하고 jest, mocking 을 통해 단위 테스트로 검증 절차를 구성하는 논리 절차에 대해 알아보았다.

실제로는 spyFn 뿐만 아니라 jest.fn( ), mockResolvedValue, mockRejectedValue 등등 모의 객체 혹은 모의 함수 구현을 할 수 있는 다양한 방법이 존재하므로 custom repository, datasource, aws 등등 다양한 의존 객체를 단위 테스트에서 적절히 처리하는 방법은 다음 포스팅에서 다뤄 보고자 한다.

Jest 의 다양한 기능들에 대한 설명은 여기로

마치며

순서가 뒤바뀌어 기존 작성된 비즈니스 로직에 대해 단위 테스트를 작성하고 있다. 물론 좋은 작성 시점은 아니긴 하지만 기존에 작성된 코드를 테스트 코드로 옮기면서 흔히들 말하는 “나쁜 냄새"를 맡을 기회가 있었다.

하나의 함수(특히 private method) 에 너무 많은 책임이 있어 테스트해야 할 케이스가 너무 많고 모호하다던가 DTO, DAO 의 역할이 명확하지 않고 레이어간 DTO 의 전달 방식, DAO 로의 변환 방식이 중구난방인 부분이라던지 …

그래서 요즘은 단위 테스트 코드를 작성하며 의도하고자 한 바를 다시 재점검하고있다. 재점검 하면서 필요없는 부분은 지우고 SOLID 원칙에 따라 리팩토링을 하고 있는데, 지금까지 느낀점은 객체지향적일 수록 단위 테스트를 하기 쉬워진다는 것이다. 이 부분도 조금 더 공부하고 생각이 정리되면 포스팅을 해볼려구 한다.

profile
개발을 사랑하는 개발자. 끝없이 꼬리를 물며 답하고 찾는 과정에서 공부하는 개발자 입니다. 잘못된 내용 혹은 더해주시고 싶은 이야기가 있다면 부디 가르침을 주세요!

0개의 댓글