본문 바로가기
source-code/software

Jest를 통한 unit test

by mattew4483 2023. 8. 18.
728x90
반응형

테스트 코드.

 

초기 스타트업 개발자, 특히 프론트엔드 개발자들이 가장 놓치기 쉬운 요소가 아닐까.

왜? → 제품의 명확한 기능이나 디자인 시스템이 갖춰지지 않은 상태로 개발이 진행되는 경우가 많기 때문.

 

끊임없이 바뀌는 요구 사항과 화면 설계서를 열심히 반영하다 보면... (거기다 기간은 어찌나 촉박한지)

기껏 작성한 테스트 코드가 쓸모 없어지는 경우가 비일비재해지며,

이런 일이 반복되면서 오히려 테스트가 생산성을 떨어뜨리는 주범이 되기도 한다.

 

현 서비스 역시 이러한 상황 속에서 운영 및 개발이 진행되어 왔는데...

초창기 때만 해도, 테스트 코드가 없다고 해서 별 다른 불편함이 느껴지지는 않았다.

구현된 결과물은 직접 서비스를 동작하면서 확인할 수 있었고 (끽해야 버튼 몇 개 클릭이니)

수정 사항이 발생해도 코드의 양이 절대적으로 적으니, 소스코드를 하나하나 확인하며 고쳐도 충분했다.

 

그러나... 서비스 운영 시간이 점차 길어지면서

기존 코드의 양은 걷잡을 수 없이 늘어났고,

서비스 내 기능들이 서로 간에 영향을 미치기 시작했으며,

추가할 기능 역시 계속해서 생겨났고,

그와 비례해 기존 코드를 수정하는 횟수 역시 기하급수적으로 증가했다.

 

즉... 

더 이상 테스트 없이 개발을 진행하기 어려운 상황에 놓이고 말았다!

그렇지만 없던 테스트 코드가 뚝딱 떨어지지는 않는 노릇.

따라서 가장 작은 단위, 즉

1) 앞으로 추가되는 기능에 대한 2) 단위 테스트 를 작성하는 것부터 시작하기로 하였다.

 

testing tool

Jest를 선택!

https://jestjs.io/

 

Jest

By ensuring your tests have unique global state, Jest can reliably run tests in parallel. To make things quick, Jest runs previously failed tests first and re-organizes runs based on how long test files take.

jestjs.io

 

 

test target

특정 미용샵의 toggle 여부와 관련된 Class를 구현하고자 했고,

GET 요청을 통해 server에서 받아온 데이터를 통해, 해당 미용샵의 toggle이 켜져있는지 반환하는 메서드를 테스트 대상으로 삼았다.

 

test code

가장 처음 작성한 테스트 코드는 다음과 같다.

import ToggleModule from "./ToggleModule";

// 가짜(Mock) API 응답을 설정
jest.mock("../../api", () => ({
  getToggleApi: jest.fn((salon_id) => {
    if (salon_id === 200) {
      return Promise.resolve({ data: { id: 0, status: true } });
    } else if (salon_id === 400) {
      return Promise.resolve({ data: { id: 1, status: false } });
    } else if (salon_id === 401) {
      return Promise.resolve({ data: false });
    } else {
      return Promise.reject("error");
    }
  }),
}));

describe("미용샵 id를 통한 toggle 상태 조회", () => {
  test("토글 켜짐", async () => {
    const salonId = 200;
    const toggleModule = new ToggleModule();
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(true);
  });

  test("토글 꺼짐", async () => {
    const salonId = 400;
    const toggleModule = new ToggleModule();
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(false);
  });

  test("토글 존재X", async () => {
    const salonId = 401;
    const toggleModule = new ToggleModule();
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(false);
  });

  test("server 에러 발생", async () => {
    const salonId = 500;
    const toggleModule = new ToggleModule();
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(false);
  });
});

ToggleModule의 isToggleOn이란 메서드를 테스트 대상으로 삼았으며,
해당 메서드는 내부적으로 getToggleApi라는 비동기 함수를 통해 서버로부터 toggle 상태에 대한 데이터를 반환받고,

이를 통해 (기타 비즈니스 로직을 거친 후) 해당 미용샵의 toggle 여부를 boolean으로 반환하는 역할!

 

// 가짜(Mock) API 응답을 설정
jest.mock("../../api", () => ({
  getToggleApi: jest.fn((salon_id) => {
    if (salon_id === 200) {
      return Promise.resolve({ data: { id: 0, status: true } });
    } else if (salon_id === 400) {
      return Promise.resolve({ data: { id: 1, status: false } });
    } else if (salon_id === 401) {
      return Promise.resolve({ data: false });
    } else {
      return Promise.reject("error");
    }
  }),
}));

이때 현재 server가 올바르게 동작하는지는 해당 단위 테스트의 관심사가 아니므로,

server 측 반환값 명세에 기반해 mock api를 작성한 모습.

(해당 반환값은 1)토글을 켠 샵 2)토글을 끈 샵 3)토글을 제어조차 하지 않은 샵  4)서버 에러 의 네 가지 경우를 가진다)

 

문제점

테스트가 의도대로 잘 동작하긴 하지만, 여러 개선 사항이 존재한다.

 

반복되는 생성자 함수

각 테스트 마다 테스트 대상인 Class의 생성자 함수를 호출하는 구문(new ToggleModule())이 중복되고 있다.

해당 테스트는 ToggleModule이란 Class를 테스트하기 위해 작성되어 있으므로, 모든 테스트마다 해당 Class 객체를 생성하는 것이 당연!

따라서 모든 테스트가 실행 전에 해당 Class를 생성하면 좋을 것 같다.

→ Jest의 beforeEach 메서드를 통해 각 test 실행 전 동작할 코드를 작성할 수 있다!

// 가짜(Mock) API 응답을 설정
...

describe("미용샵 id를 통한 toggle 상태 조회", () => {
  let toggleModule;
  beforeEach(() => {
    // 각 test문 실행 전 초기화
    toggleModule = new ToggleModule();
  });

  test("토글 켜짐", async () => {
    const salonId = 200;
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(true);
  });

  ...
});

해당 describe문 내부 test가 실행되지 전, 각 Class를 초기화 해준 모습!

 

MockApi 의존

그러나 위 테스트 코드에는 치명적인 문제점이 존재한다.

현재 getToggleApi란 이름의 비동기 요청 함수를 mocking해 사용하고 있는데,

해당 mock 함수는 인자로 넘어온 salon_id에 따라 해당 상황에 맞는 데이터를 반환하고 있다.

// 가짜(Mock) API 응답을 설정
jest.mock("../../api", () => ({
  getToggleApi: jest.fn((salon_id) => {
    if (salon_id === 200) {
      return Promise.resolve({ data: { id: 0, status: true } });
    }
    // ...
  }),
}));


// ...
  test("토글 켜짐", async () => {
    const salonId = 200;
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(true);
  });
// ...

즉 '토글 켜짐'을 테스트 할 때는, 200이라는 id를 mock 함수에 넘겨준 후,

우리가 지정한 켜짐 상태에 해당하는 데이터를 토대로, 테스트 결과를 반환하게 된다.

 

그런데 이렇게 되면..?

200이라는 id가 '켜짐 상태에 해당하는 데이터'를 반환할 것이라는 건 어떻게 테스트할 것인가??? 라는,

 테스트 코드를 위한 테스트 코드를 위한 테스트 코드가 필요한 상황...에 놓이게 된다.

 

각 테스트는 상호 의존적이지 않아야 한다는 원칙이 깨진 것이며,

이 경우 테스트가 외부 요소(이 경우에는 api를 mocking 하는 테스트 외부 메서드)에 의존하기 때문에 발생한 문제라 할 수 있다.

이를 해결하기 위해서는? → 의존성을 분리하면 되겠다!

import ToggleModule from "./ToggleModule";
import { getToggleApi } from "../../api"; // mocking할 함수 import

// 가짜(Mock) API 응답을 설정
jest.mock("../../api", () => ({
  getToggleApi: jest.fn(),
}));

getToggleApi란 비동기 함수는 jest.fn()을 이용한 mock function으로 mocking 할 뿐이며,

 

describe("미용샵 id를 통한 toggle 상태 조회", () => {
  test("토글 켜짐", async () => {
    getToggleApi.mockResolvedValueOnce({
      data: { id: 0, status: true },
    }); // 해당 test 내부에서 getToggleApi에 기대하는 반환 데이터 작성
    const salonId = 200; // 임의의 값이여도 무관해짐
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(true);
  });

  test("토글 꺼짐", async () => {
	getToggleApi.mockResolvedValueOnce({
      data: { id: 0, status: false },
    });
    const salonId = 400;
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(false);
  });

  test("토글 존재X", async () => {
  	getToggleApi.mockResolvedValueOnce({
      data: false,
    });
    const salonId = 401;
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(false);
  });

  test("server 에러 발생", async () => {
    getToggleApi.mockRejectedValueOnce(new Error("Test error"));
    const salonId = 500;
    const result = await toggleModule.isToggleOn(salonId);
    expect(result).toBe(false);
  });
});

해당 반환값은 mock function의 mockResolvedValueOnce와 mockRejectedValueOnce 메서드를 통해

각 테스트 내부에서 지정해 준 모습!

 

이를 통해 각 개발자들은 더 이상 외부에서 작성된 로직이 아닌
해당 구문 내부 로직을 통해 각 테스트를 이해하고, 수정할 수 있을 테다.

 

import ToggleModule from "./ToggleModule";
import { getToggleApi } from "../../api"; // mocking할 함수 import

// 가짜(Mock) API 응답을 설정
jest.mock("../../api", () => ({
  getToggleApi: jest.fn(),
}));

describe("미용샵 id를 통한 toggle 상태 조회", () => {
  let toggleModule;
  beforeEach(() => {
    // 각 test문 실행 전 초기화
    toggleModule = new ToggleModule();
  });

  test("토글 켜짐", async () => {
    getToggleApi.mockResolvedValueOnce({
      data: { id: 0, status: true },
    }); // 해당 test 내부에서 getToggleApi에 기대하는 반환 데이터 작성

    const salonId = 200; // 임의의 값이여도 무관해짐
    const result = await toggleModule.isToggleOn(salonId);
    
    expect(result).toBe(true);
  });

  test("토글 꺼짐", async () => {
	getToggleApi.mockResolvedValueOnce({
      data: { id: 0, status: false },
    });
    
    const salonId = 400;
    const result = await toggleModule.isToggleOn(salonId);
    
    expect(result).toBe(false);
  });

  test("토글 존재X", async () => {
  	getToggleApi.mockResolvedValueOnce({
      data: false,
    });
    
    const salonId = 401;
    const result = await toggleModule.isToggleOn(salonId);
    
    expect(result).toBe(false);
  });

  test("server 에러 발생", async () => {
    getToggleApi.mockRejectedValueOnce(new Error("Test error"));
    
    const salonId = 500;
    const result = await toggleModule.isToggleOn(salonId);
    
    expect(result).toBe(false);
  });
});

완성된 테스트 코드!

728x90
반응형

'source-code > software' 카테고리의 다른 글

클린 코드  (0) 2023.08.20
클린 아키텍처  (0) 2023.08.20
SEO  (1) 2021.07.28
API 활용하기  (0) 2021.05.23
Lighthouse로 performance 측정하기  (0) 2021.05.12