Test Code 왜 필요한가?

sikkzz·2024년 7월 30일
0

React

목록 보기
12/12
post-thumbnail
post-custom-banner

🖐️ 시작하며

프로젝트를 진행하면서 기획 및 설계를 진행하고 디자인과 api가 나온 이후 요구 사항에 맞춰서 개발을 진행하게 됩니다. 볼륨이 거대한 프로젝트일수록 요구사항은 복잡하고 많기 때문에 최대한 꼼꼼하게 코드를 작성한다 해도 빼먹거나 요구 사항과는 다르게 만든 기능들이 존재할 수 있습니다.

저는 보통 개발을 진행할 때 UI 구성 -> api 연결 -> 예외 처리 구현 -> 최종 테스트와 같은 과정으로 진행합니다.

정답이 존재하지는 않기에 다른 방법들도 존재하겠지만 그렇다고 꼭 이렇게 해야한다는 신념을 가지고 개발을 진행하는건 아닙니다.

UI를 구성하면서 api 및 데이터에 대해 설계를 그려보고 UI와 api에 구현이 완료되면 이제 disabled 조건이나 렌더링 조건, 에러 핸들링 등 여러 예외처리를 진행하고 요구 사항에 부합한지 최종 테스트 진행 후 해당 기능 구현을 마무리 합니다.

최근 진행중인 프로젝트에는 PM과 서브 PM이 존재하고 기존 프론트 파트 개발자가 Q&A 쪽으로 방향을 틀면서 Q&A 담당자까지 존재합니다. 이러한 사람들이 없다고 Q&A를 진행되지 않는건 아니지만 맡아서 담당하는 사람들이 있기에 더욱 세세하게 진행되고 있습니다.

사실 이전 프로젝트들은 제대로 Q&A가 진행되지는.. 흠흠

이러한 환경과 더불어 최근 가장 관심을 가지고 있는 주제중 일부였던 jest, cypress, storybook등의 학습을 위해 테스트 코드 작성에 눈독을 들이기 시작했습니다.

사실 많은 레퍼런스들을 찾아보려고 노력했지만 실무단이 아닌 오픈 소스로 나와있는 프로젝트들에는 테스트 코드가 완벽히 작성된 레퍼런스를 찾기는 어려웠습니다. 아쉬움이 컸지만 제쳐두고 먼저 테스트 코드의 개념과 더불어 프론트 파트에서 테스트 코드는 어떻게 작성되는지에 대해 알아보겠습니다.

테스트코드 뭐가 좋은데?

다들 아시겠지만 테스트는 우리가 만든 기능이 설계 의도대로 작동하고 있는지 확인하는 과정입니다. 실무 단위에서는 테스트 코드 작성과 더불어 Q&A가 따로 이루어지겠지만 프로젝트 단위에서는 테스트 코드는 고사하고 스프린트 완료 이후에 Q&A라도 제대로 이루어지면 다행일거라 생각합니다.

그럼 이 테스트 코드는 왜 필요할까요?

1. 디버깅 비용 절감

우리가 작성하는 코드는 사실 정답이라는게 존재하지 않습니다. 같은 기능이여도 몇백개의 코드가 나올 수 있기에 기획 및 설계에 정말 많은 시간을 투자하고 코드를 작성해도 완벽한 코드란 존재할 수가 없습니다.

그렇기에 버그 및 에러는 우리 개발자들과 평생을 함께 살아가는 존재가 됩니다. 이를 해결하기 위해 정말 많은 시간을 통해 디버깅을 진행합니다. 실제로 실무에서 날라오는 버그 및 이슈 리포트들은 몇백개는 가볍다고들 얘기하기도 합니다.

이때 만약 하나의 기능에서 버그가 발생하면 여러분은 과연 문제를 해결하는 시간과 문제가 어디서 발생했는지 찾는 시간 어떤 시간이 더 오래 걸리시나요?

보통 후자가 많지 않을까 생각합니다. 프로젝트가 커지고 기능이 많아질수록 에러 발생 시 서로 영향을 주는 구성 요소들은 배로 증가하기 때문에 다 둘러보면서 어디가 잘못된건지 하나하나 찾아야 하기 때문입니다.

테스트 코드 도입을 통해 코드의 결함을 최소화하고 오류 발생시 대처 시간 또한 최소화가 가능합니다.

테스트는 e2e, integration, unit 테스트로 나뉘는데요 각 테스트들은 이따가 자세히 알아보도록 하고 해당 테스트 경계들을 통해 문제가 되는 코드의 범위와 케이스를 빠르게 파악하고 수정할 수 있습니다.

2. 코드 수정 및 확장성 용이

혹시 에러를 고쳤는데 더 많은 에러가 발생한 경험 있으신가요? 기껏 에러가 난 곳을 열심히 찾았고 구글링을 통해 고치는데 성공했습니다. 그런데 그 코드 수정으로 인해 다른 코드들에서 무수히 많은 에러가 발생하는 경험 해보신적 있으실꺼라 생각합니다.

이를 회귀 버그라고 지칭합니다.

회귀 버그
이전에 제대로 작동하던 소프트웨어 기능에 문제가 생기는 것을 가리킨다. 일반적으로 회귀 버그는 프로그램 변경 중 뜻하지 않게 발생한다. - 위키 백과 -

정말 화가 나지만 완벽하게 차단할 수 없는 부분이기도 합니다. 우리가 만들고 사용하는 기능들은 단일 요소로만 이루어질 수 없는 기능들이 대부분입니다. 그렇기에 요소들의 상호작용은 피할 수 없고 회귀 버그 또한 따라올 수 밖에 없습니다.

정리해보면 이미 만들어진 기능의 코드를 수정하거나 새로운 기능을 추가할 때 회귀 버그가 따라올 수 밖에 없다는 얘기고 그렇다고 회귀 버그와 함께 살아갈 수는 없겠죠? 그렇기에 테스트 코드 작성을 통해 관리하고 대처해야 합니다.

테스트 코드는 당장 만드는 기능이 잘 작동하는지를 넘어서 요구사항이 변경되어 기존 코드를 수정할 때 또는
더 나은 코드를 위해 리팩토링할 때등 코드의 유지 보수 및 관리를 용이하게 만드는 수단이기도 합니다.

테스트 종류

테스트는 테스트의 범위 및 성격에 따라 e2e, integration, unit으로 나눕니다.

E2E 테스트 (End to End)

E2E 테스트는 End to End 테스트의 약자로 서비스의 흐름을 처음부터 끝까지 테스트하는 것을 의미합니다. 유닛 테스트나 통합 테스트는 모듈의 무결성을 검증할 수 있는 테스트지만 모듈의 무결성이 서비스 동작의 무결성까지는 증명할 수 없습니다.

그렇기에 E2E 테스트는 실제 사용자의 시나리오를 테스트함으로써 서비스 동작을 테스트하고 서비스의 무결성을 증명할 수 있습니다. 사용자 인터페이스를 통해 시스템의 모든 부분을 테스트하고 모든 컴포넌트가 함께 작동하는 방식을 확인하며 일반적으로 통합 테스트 후에 수행합니다.

다만 서비스 전체 시나리오를 검증해야하기에 테스트 과정이 길고 무겁습니다. 비용이 많이 들지만 반드시 필요한 테스트라는 사실은 변함이 없습니다.

E2E 테스트에는 보통 Cypress, playwright 등을 많이 사용합니다. Jest로도 작성은 가능하지만 jest로 E2E 테스트 작성 시 작은 프로젝트 단위에도 너무나 많은 코드 작성이 필요하기에 지양하는게 좋습니다.

본인도 jest로 작성하다가 포기한 경험이 있습니다..

통합 테스트 (Integration 테스트)

통합 테스트는 여러 컴포넌트 혹은 개별 모듈들이 서로 올바르게 작동하는지 확인하는 테스트 방법입니다. 각각의 요소들의 상호작용이 정상적으로 이루어지고 작동하는지를 테스트합니다.

다음에 알아볼 단위 테스트와 비슷하지만 차이점이 있다면 단위 테스트는 각 컴포넌트나 모듈을 독립적으로 테스트하는 반면 통합 테스트는 단위 테스트를 마친 요소들을 결합해서 올바르게 작동하는지를 확인하는 방법입니다.

각 요소간의 데이터 통신, 데이터 공유, 전반적인 제어 흐름 등을 확인하며 전체 애플리케이션의 안정성과 신뢰성을 높이는 데 중요한 역할을 합니다. 단위 테스트만으로는 확인하기 어려운 문제들을 통합 테스트를 통해 미리 발견하고 대응할 수 있습니다.

다만 통합 테스트만으로도 전체 서비스의 무결성을 테스트하기에는 어렵다는 점, 또한 유닛 테스트가 선행적으로 이루어져야 통합 테스트가 원활하다는 점을 고려하면 결국은 단위 테스트와 E2E 테스트 등 다른 유형의 테스트와 함께 이루어져야한다는 점을 알 수 있습니다.

주로 msw를 활용해서 통합 테스트 진행을 많이 하지만 코드가 복잡하고 비용이 크기에 꼭 필요한 과정이 아니라면 유닛 테스트에 집중하고 E2E 테스트로 마무리하는 편이 좋다고 합니다.

단위 테스트 (Unit 테스트)

단위 테스트는 서비스를 구축하는데 가장 작은 단위인 함수, 메서드, 클래스, 모듈 등이 의도한대로 올바르게 작동하는지를 확인하는 테스트 방법입니다.

각 단위들의 테스트를 통해 서비스 전체의 안정성과 신뢰성을 높이는데 중요한 역할을 합니다. 단위 테스트를 통해 코드의 유지 보수를 쉽게 만들어주며 기능 수정, 새로운 기능 추가 과정에서 신규 테스트 케이스 추가나 기존 테스트 케이스를 수정할때도 유용합니다.

단위 테스트는 범위가 한정적이므로 단위에 대한 테스트 코드의 양이 적고 비용도 작습니다. 다만 요구 사항이나 명세서가 변경된다면 가장 많이 영향을 받는 테스트이기에 설계 과정을 신중히 할 필요가 있씁니다.

단위 테스트는 주로 테스트 주도 개발(Test-Driven Development, TDD) 프로세스를 따라갑니다. TDD는 테스트를 먼저 작성하고 테스트 코드를 기반으로 서비스 코드를 작성하는 방법으로 설계를 개선하고 코드의 유지보수에 유리합니다.

단위 테스트는 주로 Jest를 많이 사용합니다. 단위 테스트만으로도 모든 종류의 버그를 찾아낼 수는 없습니다. 여러 요소가 상호작용할 때 발생하는 문제들도 상당히 많기 때문에 결국은 통합 테스트나 E2E 테스트를 같이 사용해야 합니다. 단위 테스트를 마친 요소들을 통합 테스트나 E2E 테스트를 통해 완벽한 서비스 구축이 가능합니다.

좋은 테스트 코드 작성법은?

테스트 코드를 아무렇게나 작성해서 서비스를 구축해도 서비스 운영에는 문제가 없을 수 있습니다. 다만 서비스 제작 과정 부터 시작해서 완성 이후 유지보수 및 관리까지 효율적이고 안정성 있는 서비스를 위해서는 테스트 코드 설계가 상당히 중요합니다. 효율적인 테스트 코드 작성을 위해서는 어떤 부분들을 지키면 좋을지 알아보겠습니다.

TDD (Test-Driven development)

TDD는 Test-Driven development의 약자로 테스트 주도 개발 이라고 부릅니다. TDD는 실제 서비스 코드 작성 이전에 테스트 코드를 먼저 작성하는 개발 프로세스입니다.

TDD는 보통 다음과 같은 순서로 진행됩니다.

  • Red: 테스트를 먼저 작성합니다.
// sum 함수가 없으므로 테스트 실패

it("a + b", () => {
  expect(sum(1, 3)).toBe(4);
});
  • Green: 테스트가 동작하고 코드를 작성합니다.
// 테스트 통과만을 위한 함수 작성

const sum = (a, b) => {
  return 4;
}

// sum 함수는 무조건 4를 반환하므로 테스트 성공

it("a + b", () => {
  expect(sum(1, 3)).toBe(4);
});
  • Refactor: 테스트의 보호 아래 코드를 리팩토링합니다.
// sum 함수를 a + b 값을 반환하도록 리팩토링

const sum = (a, b) => {
  return a + b;
}

it("a + b", () => {
  expect(sum(1, 3)).toBe(4);
});

위 예제는 너무나 간단한 코드이기 때문에 전혀 필요성을 못 느낄 수 있습니다. 하지만 기능이 조금만 복잡해지더라도 해당 기능에 대한 예외 케이스들이 너무나 많이 생기게 됩니다. 해당 기능의 테스트 코드를 작성해놓고 서비스 코드를 작성해 놓는다면 요구사항 변경 시 코드 변경이나 새로운 기능 추가 등 코드의 유지보수 및 관리에 용이해집니다.

F.I.R.S.T 원칙

F.I.R.S.T 원칙은 단위 테스트 코드에 대해 더 나은 코드를 만들 수 있는 원칙에 대해 설명합니다.

  • Fast: 단위 테스트는 빨라야 한다.
  • Isolated: 단위 테스트는 외부 요인에 종속적이지 않고 독립적으로 실행되어야 한다.
  • Repeatable: 단위 테스트는 실행할 때마다 같은 결과를 만들어야 한다.
  • Self-validating: 단위 테스트는 스스로 테스트를 통과했는지 아닌지 판단할 수 있어야 한다.
  • Timely/Thorough
    • Timely: 단위 테스트는 프로덕션 코드가 테스트에 성공하기 전에 구현되어야 한다.
    • Thorough: 단위 테스트는 성공적인 흐름뿐만 아니라 가능한 모든 에러나 비정상적인 흐름에 대해서도 대응해야 한다.

테스트 코드는 DAMP하게

테스트 코드는 DAMP(Descriptive And Meaningful Phrases)하게 작성하라는 이야기들이 많습니다. 테스트코드를 서술적이고 의미 있게 작성하라는 내용인데 간단하게 얘기하면 테스트 코드를 읽기 쉽고 이해하기 쉽게 작성하라는 내용입니다. 어떻게 보면 당연시한 얘기이기도 합니다.

다만 DAMP하게 코드를 작성하다보면 DRY 원칙과 충돌하는 경우가 자주 생깁니다. DRY 원칙은 Don't Repeat Yourself의 약자로 코드를 반복하지 말라는 원칙입니다. 일반적인 서비스 코드는 DRY 원칙을 지켜서 중복을 줄이려고 노력하겠지만 테스트 코드는 중복이 발생하더라도 누군가가 보기에 직관적이고 명확하게 이해할 수 있는 코드를 작성하는게 좋습니다.

it("submit 버튼 클릭 시 handleSubmit 이벤트가 호출됩니다.", async () => {
  const user = userEvent.setup();
  
  render(<Component {...props} />);
  
  const buttonElement = screen.getByRole("button", { name: "submit 버튼" });
  await user.click(buttonElement);

  expect(handleSubmit).toHaveBeenCalledTimes(1);
});


it("submit 버튼 클릭 시 handleSubmit 이벤트가 호출됩니다.", async () => {
  const handleSubmit = vi.fn();
  const user = userEvent.setup();
  
  render(<Component {...props} handleSubmit={handleSubmit} />);
  
  const buttonElement = screen.getByRole("button", { name: "submit 버튼" });
  await user.click(buttonElement);

  expect(handleSubmit).toHaveBeenCalledTimes(1);
});

위 두 코드는 동일한 기능에 대한 테스트 코드입니다. 해당 코드들이 테스트 코드를 DAMP하게 작성한 간단한 예시입니다. 현재는 테스트 코드에 대한 설명이 적혀있지만 만약 설명이 없다는 전제 하에 두 번째 테스트 코드가 직관적으로 이해할 수 있다는 것을 알 수 있습니다.

Given-When-Then

주어진 기능에 대한 결과를 검증해야 하는게 목적인 테스트 코드는 Given-When-Then 패턴을 적용시키면 더 좋은 코드를 구성할 수 있습니다.

Given-When-Then 패턴은 BDD(Behavior Driven Development)의 중심인 사용자 행위를 기반으로 테스트 시나리오를 정의할 수 있게 도움을 줍니다.

  • Given: 테스트를 위한 세팅된 환경 (UI 렌더링)
  • When: 테스트를 하기 위한 조건 (유저의 특정 행동)
  • Then: 테스트 결과 검증 및 확인 (결과 도출)
it("버튼 클릭 시 버튼 클릭 문구 출력", async () => {
  // Given: 화면에 버튼 렌더링 완료
  const user = userEvent.setup();
  render(<Button />);
         
  // When: 유저가 버튼 클릭
  const buttonElement = screen.getByRole("button", { name: "버튼 클릭" });
  await user.click(buttonElement);

  // Then: 버튼 클릭 문구 출력 검증
  expect(screen.getByText("버튼 클릭")).toBeInTheDocument();
});

테스트 코드의 목적을 명확히

테스트 코드를 작성할때 해당 테스트 코드가 검증해야 하는 목적을 명확히 하고 동작 코드를 검증해야합니다. 우선 컴포넌트 코드 하나를 예시로 보겠습니다.

// store.ts
interface UseTextStoreType{
  text: string;
  changeText: (newText: string) => void;
}

const useTextStore = create<UseTextStoreType>((set) => ({
  text: "테스트 코드 작성 하자",
  updateText: (newText) => set({ text: newText }),
}));

// TextButton.tsx
const TextButton = () => {
  const text = useTextStore((state) => state.text);
  const changeText = useTextStore((state) => state.useTextStore);
  
  return (
    <main>
      <h1>
        {text}
      </h1>
      <button
        type="button"
        onClick={() => changeText("테스트 코드 작성 안해")}
      >
        테스트 코드 버튼
      </button>
    </main>
  );
}

코드를 보면 쉽게 알 수 있으시겠지만 위 코드는 heading의 문구가 처음에는 "테스트 코드 작성 하자"이다가 버튼 클릭 시 "테스트 코드 작성 안해"로 문구가 바뀌는 코드입니다.

해당 컴포넌트에 테스트 코드를 작성한다면 버튼 클릭시 heading 문구가 바뀌는지를 검증해야합니다. 그렇다면 어떻게 테스트 코드를 작성할 수 있을까요?

// 첫 번째 테스트 코드

it("테스트 코드 작성 안해 문자열로 changeText 호출 시 text가 테스트 코드 작성 안해로 변경된다", () => {
  const { result } = renderHook(() => useTextStore());
  
  act(() => {
    result.current.changeText("테스트 코드 작성 안해");
  });
  
  expect(result.current.text).toBe("테스트 코드 작성 안해");
});

첫 번째 테스트 코드는 text라는 상태가 정상적으로 변경되는지 테스트하는 코드입니다. 얼핏 보면 우리가 원하는 테스트 코드의 목적에 부합하지 않나 라고 생각할 수 있지만 그렇지 않습니다.

우리가 테스트하려는건 버튼 클릭 시 heading 문구가 바뀌는지이지만 첫 번째 테스트 코드가 검증하고 있는건 내부 로직인 zustand 라이브러리를 활용한 store의 상태 업데이트를 검증하고 있습니다.

우리는 컴포넌트의 동작이 정상적으로 이루어지는지를 검증해야 하는데 이 테스트 코드는 내부 로직이 정상인지를 검증하고 있는 것입니다. 컴포넌트를 리팩토링해서 만약 zustand가 아닌 recoil이나 redux등 타 라이브러리로 상태 관리를 변경하게 된다면 테스트 코드 또한 바로 바꿔야 하는 코드임을 알 수 있습니다.

// 두 번쨰 테스트 코드

it("버튼 클릭 시 text가 테스트 코드 작성 안해로 바뀐다", async () => {
  const user = userEvent.setup();
  
  render(<TextButton />);
         
  await user.click(screen.getByRole("button", { name: "테스트 코드 버튼" }));

  expect(screen.getByText(/테스트 코드 작성 안해/i)).toBeInTheDocument();
});

두 번째 테스트 코드는 사용자의 클릭 이벤트도 있고 텍스트의 변경 확인 코드 또한 존재합니다. 이 코드 또한 원하는 목적에 부합하는 테스트 코드 같지만 아닌 부분이 있습니다.

expect 검증 코드를 보시면 올바른 문구로 변경되었는지 검증하는게 아닌 screen 내부에 "테스트 코드 작성 안해"라는 단어가 있는지를 확인하고 있습니다.

어느 정도 목적에 부합하긴 하지만 만약 "테스트 코드 작성 안해"라는 단어가 screen 다른 곳에 노출되거나 텍스트가 "테스트 코드 작성 안해 아니 해볼까?"같은 내용으로 변화가 있다면 테스트가 실패할 것입니다. 결국 테스트 목적에 맞게 코드 동작 검증을 올바르게 하는지 설계를 항상 고민해볼 필요가 있습니다.

// 세 번째 테스트 코드

it("버튼 클릭 시 heading 영역의 문구가 테스트 코드 작성 안해로 변경된다", async () => {
  const user = userEvent.setup();
  
  render(<TextButton />);
  
  await user.click(screen.getByRole("button", { name: "테스트 코드 버튼" }));

  expect(screen.getByRole("heading", { name: "테스트 코드 작성 안해" })).toBeInTheDocument();
});

세 번째 테스트 코드를 보면 비로소 정상적으로 모든 동작을 검증함을 알 수 있습니다. 버튼을 눌렀을 때 heading 영역의 문구가 테스트 코드 작성 안해 텍스트로 변경됨을 검증하고 있습니다. 첫 번째, 두 번째 테스트 코드와는 다르게 내부 동작을 검증하는게 아닌 사용자의 동작에 따라 기능이 정상적으로 이루어지는지를 테스트하고 있습니다.

결론적으로 테스트 코드를 설계할 때 고려해야할 부분이 정말 많다는걸 알 수 있습니다. 기능이 정상적으로 동작하는지를 검증해야 하지만 내부 라이브러리 동작이 아닌 컴포넌트 자체의 동작을 검증할 수 있는지를 고려하면서 컴포넌트가 변경될 때 테스트 코드에 변경이 없는게 가장 좋은 코드가 될 것입니다.

추가적으로 expect 구문 검증 과정에서도 해당 컴포넌트가 어떤 동작을 정확히 검증해야하는지를 여러 케이스들을 생각해보면서 고려할 필요가 있습니다. 이러한 내용들을 잘 고려하면서 테스트 케이스의 설계를 진행해야 합니다.

테스트 코드의 설명은 간결. 명확.

당연한 얘기일 수 있지만 테스트 코드의 설명은 중요합니다. 테스트 코드 작성 자체가 추가적인 노력을 기하는 작업이기에 테스트 코드 작성 시 설명을 대충 쓰거나 추상적으로 작성하는 경우가 있습니다.

물론 개발한 사람 입장에서는 조금 추상적으로 작성해도 이해하는데 어려움이 없지만 제3자가 코드를 확인하는 경우를 고려해야 하고 설명 작성을 잘한다면 이후 코드 수정 및 확장에 용이해지는 경우가 많습니다.

다만 설명을 작성할 때는 구현된 기능을 바라보는 시선이 아닌 컴포넌트 전체를 외부에서 바라본다는 느낌으로 작성하는게 좋습니다. 구현된 기능 위주로 작성하다보면 기능 코드를 해석하는 테스트 코드 설명이 작성될 수 있기 때문입니다. 위 내용에서 알아본바와 같이 내부 기능 동작이 아닌 컴포넌트 기능 동작을 고려해야 하기 때문에 컴포넌트가 어떻게 동작해야 하는지를 고려하면서 테스트 코드 케이스 설명들을 작성하는게 좋습니다.

const isBornIn2000OrLater = (input: string) => {
  return ["3", "4", "7", "8"].includes(digits[0]);
}

위 함수는 주민등록번호 뒷자리를 받아 2000년 이후에 태어났는지를 판단하는 함수입니다. 해당 함수에 대한 2개의 테스트 코드 예시가 있습니다.

(1)
it('입력한 문자열의 첫자리가 3,4,7,8일 때는 true를, 이외엔 false를 반환합니다', () => {
  expect(isBornIn2000OrLater('1234567')).toBe(false);
  expect(isBornIn2000OrLater('2134567')).toBe(false);
  expect(isBornIn2000OrLater('3124567')).toBe(true);
  expect(isBornIn2000OrLater('4123567')).toBe(true);
  expect(isBornIn2000OrLater('5123467')).toBe(false);
  expect(isBornIn2000OrLater('6123457')).toBe(false);
  expect(isBornIn2000OrLater('7123456')).toBe(true);
  expect(isBornIn2000OrLater('8123456')).toBe(true);
});

(2)
it('2000년 이후에 태어난 사람의 주민번호 뒷자리인지 검증합니다.', () => {
  expect(isBornIn2000OrLater('1234567')).toBe(false);
  expect(isBornIn2000OrLater('2134567')).toBe(false);
  expect(isBornIn2000OrLater('3124567')).toBe(true);
  expect(isBornIn2000OrLater('4123567')).toBe(true);
  expect(isBornIn2000OrLater('5123467')).toBe(false);
  expect(isBornIn2000OrLater('6123457')).toBe(false);
  expect(isBornIn2000OrLater('7123456')).toBe(true);
  expect(isBornIn2000OrLater('8123456')).toBe(true);
});

당연히 테스트 코드를 (1)번 처럼 작성하시는 분은 없을 거라 생각합니다. 다만 예시를 위해 말씀드리자면 두 테스트 코드의 결과값은 같습니다. 함수명때문에 기능 동작의 예측이 가능하지만 만약 함수명이 TestFunction이고 (1)번의 테스트 코드만 존재한다면 해당 함수는 진짜 입력한 문자열의 첫자리가 3,4,7,8인지 검증하려고 만든 함수라고 생각될 뿐, 2000년 이후 태어난 사람의 주민번호 뒷자리인지 검증하는 함수인지는 꿈에도 모를거라고 생각합니다.

이와같이 간결하고 명확한 테스트 코드 설명은 결과 검증과 더불어 해당 코드의 목적성을 정확히 나타낼 수 있으므로 추상적이거나 애매모호한 설명이 아닌 간결하고 명확한 설명을 작성해야 합니다.

🔚 마치며

지금까지 테스트 코드의 개념과 관련 내용들을 다뤄보았습니다. 테스트 코드에 대해 알아봤지만 테스트 코드 이전에 서비스나 프로덕트 코드에서 좋은 코드란 다들 무엇이라고 생각하시나요?

개인 견해마다 다르겠지만 저는 좋은 코드의 1순위는 남이 읽기 쉬운 코드라고 생각합니다. 더 나아가서 개발을 모르는 비개발자나 학생들도 봤을 때 대충 이러이러한 내용이지 않을까?를 알 수 있는 코드라면 더할 나위 없을 거 같습니다. 이외에도 재사용성 높은 코드, 간결한 코드 등 여러 좋은 코드들이 있겠지만 남이 읽기 쉬운 코드가 제일이지 않을까 싶네요.

테스트 코드를 작성한 사람과는 별개로 후임자나 제3자가 코드를 이어 받았을 때 아무리 읽기 쉬운 코드에다 문서까지 있어도 모든걸 100퍼센트 이해하기는 어려울 수 있다고 생각합니다. 이때, 테스트 코드가 있다면? 게다가 그 테스트 코드들은 위에서 알아본 기능이 어떻게 동작하는지 검증하는 코드와 더불어 간결하고 명확한 설명까지 포함되어 있다면 코드를 이어받아서 이해하고 유지보수 및 관리에 용이할 거라고 생각합니다.

오픈 소스로는 확인이 거의 불가능하지만 실무에서는 기능에 대한 테스트 코드가 없는 코드가 존재할 수 없을 거라 생각합니다. 다루는 사람이 많아지고 프로젝트에 대한 규모가 커질수록 테스트 코드의 필요성이 더욱 커지기 때문입니다.

이론적인 개념에 대해 알아봤으니 다음에는 직접 테스트 코드를 작성해보고 프로젝트에 어떻게 접목 시켰는지로 돌아오도록 하겠습니다.

감사합니다.

참조

단위 테스트로 복잡한 도메인의 프론트 프로젝트 정복하기(feat. Jest) - 우아한 기술 블로그
https://techblog.woowahan.com/8942/

코드와 함께 살펴보는 프론트엔드 단위 테스트 – Part 1. 이론 편 - 우아한 기술 블로그
https://techblog.woowahan.com/17404/

MSW를 활용하는 Front-End 통합테스트 - 카카오 엔터테이먼트 기술 블로그
https://fe-developers.kakaoent.com/2022/220825-msw-integration-testing/

E2E 테스트 도입 경험기 - 카카오 엔터테이먼트 기술 블로그
https://fe-developers.kakaoent.com/2023/230209-e2e/

profile
FE Developer
post-custom-banner

0개의 댓글