Development Study/Frontend

<Jest, Unit Test> 쉽고 빠르게 단위테스트 알아보기

  • -
728x90

들어가며

굳이 테스트 코드가 필요해?? 그냥 실행해보면서 개선사항들을 찾으면 안되려나?

나는 지금까지 프론트엔드 개발을 접해오면서 이런 생각을 해왔다. 그도 그럴것이 백엔드보다는 테스트가 덜 중요한 것이 이 프론트엔드였고, 프론트엔드에서는 렌더링만 잘하고 화면을 멋지게 잘 구성하기만 하면 된다라는 인식이 널리 퍼져있기 때문이었다.
하지만 이는 내 오판이었다. 실제로 개발을 하면서 규모가 커지고 복잡해지자 어디서부터 오류가 발생하였는 지 찾는 데 걸리는 시간이 훨씬 더 오래 걸리게 되었고, 테스트코드를 작성하지 않음으로 인해 생겨나는 디버깅 시간이 테스트코드를 작성하는 데 걸리는 시간보다 더 길게 걸린다는 사실을 얼마 뒤 알 수 있었다.

그렇다면 테스트코드, 왜 필요한 것이고 어떻게 작성해야 할까?
우아한 테크코스 프리코스를 해오며 깨달은 지금까지의 인사이트를 정리해보도록 하겠다.


테스트코드? 그건 뭐야?

JavaScript에서 테스트 코드는 개발 과정의 중요한 부분 중 하나로, 개발자들이 작성한 코드가 예상대로 작동하는지 확인하기 위해 사용되며, 아래와 같은 상황을 위해 주로 사용된다.

  1. 버그 감지
    • 개발 과정 중에 버그를 더 빠르게 찾아 수정할 수 있도록 도와준다.
    • 이는 코드의 안정성과 신뢰성을 높이는 데 기여한다.
  2. 리팩토링 지원
    • 코드를 개선하거나 업데이트할 때, 기존의 기능이 여전히 올바르게 작동하는지 확인하는 데 도움이 된다.
    • 테스트 코드가 있으면 보다 더 과감한 변경을 시도할 수 있다는 장점이 있다.
  3. 문서화 기능
    • 테스트 코드는 종종 코드의 사용 방법을 보여주는 예제로 사용된다.
    • 새로운 개발자가 프로젝트에 참여할 때, 테스트 코드를 통해 기존 코드를 더 잘 이해할 수 있다.
  4. 디자인 개선
    • 테스트를 작성하면서 개발자는 코드의 구조와 설계에 대해 더 깊이 생각하게 된다.
    • 괜히 테스트를 위한 코드를 작성하는 것, TDD 등의 개념이 나온 것이 아니며,
    • 더 깔끔하고 유지보수하기 쉬운 코드를 작성하는 데 도움을 준다.

JavaScript에서 테스트 코드를 작성하기 위한 인기 있는 도구로는 Jest, Mocha, Jasmine 등이 있다.
여러 테스트 방법들이 있으며, 그 중 많이 쓰이는 테스트로는 단위 테스트(unit tests)에서부터 통합 테스트(integration tests) 및 E2E(End-to-End) 테스트가 있다.


테스트코드를 작성하지 않아서 생겼던 문제들

테스트코드를 작성하지 않았다는 소리는 내가 시험을 보고 채점을 하지 않았다는 소리와 같다.

실제로도 그랬다. 정말 채점을 하지 않았던 것처럼 나중에 같은 문제가 나왔을 때 틀렸던 부분에서 다시 틀리는 것처럼 그러한 문제가 발생했던 것이다.

위에서 알아본 테스트코드를 작성해야하는 이유에서 알 수 있듯이 실제로 돌아가는 데에서도 문제가 발생하는 가장 근본적인 이유에서부터 같은 프로젝트를 다른 사람들에게 합류를 요청하고 제안을 해야하는데 실제로도 테스트코드가 없어서 각 기능들을 문서화 해야했고, 이는 다른 사람들이 코드를 이해하는 데 더욱 많은 시간을 소요하게 만들기도 했다.


그렇다면 테스트코드는 어떻게 작성해야할까?

프론트엔드는 테스트코드가 비교적 중요도가 떨어진다던데, 굳이 작성 해야해?

실제로도 테스트코드가 백엔드에 비해 덜 중요한 것은 사실이다. 물론 어디까지나 "상대적으로"를 의미하기에 결국은 중요하지 않다고 이해하는 것은 옳지 않다.

이 글에서는 간단하게 단위 테스트를 작성하는 방법 몇 가지를 소개해보겠다.


1. 기본적인 테스트 구조

테스트는 기본적으로 "내가 이 입력값을 넣을테니 이런 결과가 나오는지 확인해줘" 라는 말을 만족시키기 위해 돌아간다.

이는 실제 테스트코드 예시를 통해 알 수 있는데, 그 중 한가지 구조를 보도록 하겠다.

describe("DragonFly 모델 테스트", () => {
  test("입력받은 값이 비어있는 값인지 확인", async () => {
    const input = "";
    expect(() => {
      new DragonFly(input);
    }).toThrow(ERROR_CONVENTION);
  });
});

위 코드에서 볼 수 있듯 "describe"를 통해 어떤 것에 대한 테스트를 진행할 지 "묘사"하고, "test"를 통해 어떤 테스트를 진행해볼 지 상세하게 "설명"하며, "expect to ..."를 통해 기대하는 바가 제대로 출력되는지 확인해본다
문장으로 다시 변경해보겠다.

Describe "What to test", Test "How to test", expect to (Be, Equal, Throw ...) specific result

무슨 말인지 한번에 이해하기 힘들다면? 아래에 어떻게 작성할 수 있는 지 확인해보자.


단위테스트 기본원칙

단위테스트는 가장 기본적인 로직에 대해 진행하는 근본적인 테스트이다.

쉽게말해 단위테스트는 시스템 흐름을 살펴보는 것이 아닌 시스템 흐름에 들어가있는 기능 하나하나에 대한 테스트를 진행하는 것이다.

아래에 여러 설정을 확인해보겠다.

  1. 단위 테스트는 최소한의 기능을 가진 함수에 대한 테스트이다.
    • 여기서 기본적으로 테스트를 하기 위해 최소한의 기능만을 가지고있는 지 확인할 수 있다.
  2. 단위 테스트를 하기 위해 보다 상세하게 테스트 목적을 적어야 한다.
    • 테스트 목적이 상세하면 상세할 수록 코드 유지보수 및 시간이 흘렀을 때 나 자신 혹은 다른 사람들이 기존의 코드를 보고 이해하는 시간이 단축된다.
  3. 단위 테스트를 작성할 때 변수명 또한 가급적 이해하기 쉽게 작성한다.
    • "input", "output"보다는 "numberData", "expectedStringData" 처럼 보다 읽었을 때 파악하기 쉽도록 작성한다.

단위테스트 작성하기: 예상하는 결과가 단일 값일 경우

describe("테스트의 단위를 설명해두는 부분", () => {
  test("단위 내 상세 테스트 정보를 적어두는 부분", () => {
      // 이쯤 빈공간에 변수들 선언또는 생성해두기
    expect(expectedValue).toBe(singleValue); // 단일 값의 경우 .toBe()
  });

    test("...다른 테스트", () => {});
});

아래와 같은 예시를 볼 수 있다.

describe("Orange Model 테스트", () => {
  test("오렌지 이름에 '싱싱한'이 포함되어 있다면 'fresh' 라는 문자열을 반환한다.", () => {
      const orangeName = "싱싱한 제주도 오렌지";
      const testCase = checkOrangeStatus(orangeName);
      const expectedOrangeStatus = "fresh";

    expect(testCase).toBe(expectedOrangeStatus);
  });

    test("...다른 테스트", () => {});
});

단위테스트 작성하기: 예상하는 결과가 배열 또는 객체일 경우

describe("테스트의 단위를 설명해두는 부분", () => {
  test("단위 내 상세 테스트 정보를 적어두는 부분", () => {
      // 이쯤 빈공간에 변수들 선언또는 생성해두기

    expect(expectedValue).toEqual(multpleValue); // 다중 값의 경우 .toEqual()
  });

    test("...다른 테스트", () => {});
});

아래와 같은 예시를 볼 수 있다.

describe("Orange Model 테스트", () => {
  test("오렌지 이름에 '싱싱한'이 포함되어 있다면 배열에 'fresh' 라는 문자를 추가한다.", () => {
      const orangeName = "싱싱한 제주도 오렌지";
      const testCase = checkOrangeStatus(orangeName)
      const expectedOrangeStatus = ["orange", "Jeju-Island", "fresh"];

    expect(testCase).toEqual(expectedOrangeStatus);
  });

    test("...다른 테스트", () => {});
});

단위테스트 작성하기: 예상하는 결과가 에러를 던지는 것일 경우

describe("테스트의 단위를 설명해두는 부분", () => {
  test("단위 내 상세 테스트 정보를 적어두는 부분", () => {
      // 이쯤 빈공간에 변수들 선언또는 생성해두기

    expect(() => {
      new TestClass("Set Value Here");
    }).toThrow("여기의 문자열이 Throw된 메세지에 포함되는지 확인");
  });

    test("...다른 테스트", () => {});
});

아래와 같은 예시를 볼 수 있다.

describe("Orange Model 테스트", () => {
  test("오렌지 이름에 '싱싱한'이 포함되어 있지 않다면 에러를 발생한다.", () => {
      const orangeName = "쌩쌩한 제주도 오렌지";
      const testCase = checkOrangeStatus(orangeName);

    expect(checkOrangeStatus(orangeName)).toThrow("[ERROR]");
    // 던져지는 에러에 "[ERROR]"라는 문자열이 포함되어있어야 한다.
  });

    test("...다른 테스트", () => {});
});

단위테스트 작성하기 Plus: 같은 테스트를 여러 값에 대해 진행하고 싶을 때

이렇게 하나의 케이스에 하나씩 테스트를 해볼 수 있지만, 생각외로 여러 케이스에 대한 테스트를 진행해보아야 할 때가 많은 실제 요구사항에서는 테스트코드를 여러 개 복사해서 사용할 수도 없는 노릇이다.
이럴 때에는 어떻게 해야할까?

A. 아래와 같이 "test.each()"를 사용하여 메서드를 여러 개 확인해본다.

describe("calculate util 테스트", () => {
  const testCases = [
    [[100, 20], 120],
    [[123, 45], 168],
    [[1, 2, 3, 4, 5], 15],
    [[0, 0, 1, 1000, 2000], 3001],
    [[300, 10, 4000, 32], 4342],
  ];

  test.each(testCases)(
    "배열을 집어넣었을 때 배열 내의 값의 합이 정상적으로 출력되어야 한다.",
    (arrayItem, expectedResult) => {
      const testCase = sumArray(arrayItem); // sumArray 또는 해당 계산을 수행하는 함수 사용

      expect(testCase).toBe(expectedResult);
    }
  );

  test("...다른 테스트", () => {});
});
  • 배열에 들어있는 각각의 값이 orangeNames에 들어가서 테스트를 한번씩 실행시킨다. 즉, 같은 테스트에 대해 여러 값을 돌려보며 테스트할 수 있다는 것이다.

이제, 본인이 작성한 코드에 맞게 테스트를 잘 작성하여 구현한 기능이 잘 돌아가는 지 확인해보자!


End

728x90
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.