Jest와 Supertest로 테스트하기

테스트를 작성해야 하는 이유

버그 픽스, 리팩토링 등 좋은 의도에서 코드를 수정했을지 몰라도 개발자의 실수로 잘 돌아가던 코드가 에러를 발생시킬 가능성이 항상 도사리고 있다. 그렇다면 코드가 변경되었을 때 의도한 대로 동작하는지 확인할 수 있는 방법은 없을까?

테스트를 이용하면 코드의 신뢰성을 확인할 수 있다. 코드를 수정한 후 기존에 작성해놓았던 테스트 코드가 실패한다면 이는 코드가 제대로 동작하지 않는다는 걸 의미한다. 즉, 테스트는 새로운 기능을 추가하거나 기존 기능을 변경할 때 예상치 못한 문제를 방지하는 중요한 수단이다.

Jest를 선택한 이유

테스트는 쉽고 빠르게 실행할 수 있어야 한다. 테스트 실행이 번거롭고 오래 걸린다면 개발자들은 테스트하는 것을 기피할 것이기 때문이다. 이런 점에서 Jest는 큰 이점을 갖고 있다고 판단하여 테스트 라이브러리로 Jest를 선택했다.

<aside> 💡 Jest의 장점

Jest를 이용한 단위 테스트

jest로 서비스 코드를 테스트 할 것이다. 하지만 서비스 코드 안에는 데이터베이스에 접근하는 로직이 들어있다.

...
@Injectable()
export class TarotService {
  constructor(
    @InjectRepository(TarotCard)
    private readonly tarotCardRepository: Repository<TarotCard>, // 데이터베이스에 접근하는 레포지토리!!
    @InjectRepository(TarotResult)
    private readonly tarotResultRepository: Repository<TarotResult>, // 데이터베이스에 접근하는 레포지토리!!
  ) {}
  ...
}

데이터베이스를 조작하면 실제 프로세스에 부정적인 결과를 초래할 수 있다. 따라서 데이터베이스를 테스트 코드로부터 보호해야 한다.

위 문제를 해결하기 위해 목(mock)을 활용할 수 있다. 목은 함수에 대한 호출만 기록하고 어떠한 일도 수행하지 않는 것을 의미한다. 즉, 레포지토리 함수가 호출되긴 하나 데이터베이스에 접근하지 않는 것이다. 이처럼 목을 활용하면 의존성을 시뮬레이션할 수 있다.

it('해당 번호의 타로 카드가 존재하지 않아 NotFoundException을 반환한다.', async () => {
  [{ cardNo: -1 }, { cardNo: 79 }].forEach(async ({ cardNo }) => {
    const findOneByMock = jest
      .spyOn(tarotCardRepository, 'findOneBy')
      .mockResolvedValueOnce(null);
 
    await expect(
      tarotService.findTarotCardByCardNo(cardNo),
    ).rejects.toThrow(NotFoundException);
    expect(findOneByMock).toHaveBeenCalledWith({
      cardNo: cardNo,
      cardPack: undefined,
    });
  });
});

위의 테스트 코드는 타로 카드를 조회하는 메서드 findTarotCardByCardNo에 대한 테스트를 진행하고 있다. spyOn을 이용하여 레포지토리의 findOneBy 메서드를 목으로 대체한다. 그리고 mockResolvedValueOnce 함수를 이용하여 findOneBy가 리턴할 값을 지정한다. 이로써 데이터베이스에 접근하지 않고 서비스 코드를 테스트 할 수 있다.

Supertest를 이용한 e2e 테스트