React 프로젝트에 TDD 적용하기 (Using react-testing-library)

React 프로젝트에 TDD 적용하기 (Using react-testing-library)

React 프로젝트에 TDD 적용하기 (Using react-testing-library)

최근에 회사 팀원분을 통해 react-tesing-library에 대한 소개와 TDD 적용기를 간단한 세미나를 통해 들은 적이 있습니다. 짧은 시간이었지만 나는 유닛테스트 정도의 테스트 코드만 주로 작성해보았던 터라 꽤 재미있게 들었고, 좀 더 찾아보았습니다.

왜 TDD를 해야 할까?

테스트 코드가 없는 프로젝트의 경우 아래와 같은 방식으로 개발이 주로 진행됩니다.

  1. 무엇을 개발해야하는지, 무엇이 문제인지 명확히 알지 못한 상태에서 코드 작성을 하기 시작합니다.
  2. 완성되 코드가 문제없이 동작하는지 확인하기 위해 console.log() 또는 브라우저에서 직접 실행해 봅니다.
  3. 추후, 코드 변경이 일어난 경우 변경 된 코드가 문제없이 동작하는지 확인하기 위해 2번의 과정을 다시 반복합니다.

테스트 케이스가 없다면 모든 변경이 잠정적으로 버그가 될 수 있습니다. 아무리 설계를 잘하고, 유연하게 잘 짰다 하더라도 테스트 코드가 없다면 개발자는 코드 변경을 주저하게 됩니다.

테스트 코드는 개발자의 자신감 지수를 높여줍니다.

테스트 케이스가 있다면 변경이 두렵지 않습니다. 테스트 코드가 있다면 개발에 있어 자신감 지수가 높아지게 됩니다. 커버리지의 기준을 어떤 것으로 잡냐에 따라 달라지겠지만, 테스트 커버리지가 높으면 높을수록 자신감 지수는 높아집니다. 설계가 모호하고 엉망이거나, 아키텍쳐가 부실한 코드여도 별다른 우려 없이 변경할 수 있습니다. 테스트 케이스가 있으면 변경이 쉬워지기 때문입니다.

그러면 왜 TDD야?

기존 방식과 달리, TDD는 문제를 먼저 정의한 뒤에 문제의 해답을 찾아갑니다.

TDD Life Cycle

TDD는 기본적으로 실패(Red), 성공(Green), 리팩토링(Refactoring)라고 불리는 3단계 과정을 반복합니다.

  • 실패: 문제 정의 단계로, 테스트 코드를 실제 코드보다 먼저 작성합니다.
  • 성공: 문제 해결 단계로, 테스트를 통과할 수 있는 최소한의 코드를 작성합니다.
  • 리팩토링: 불필요하고 비효율 적인 코드를 개선합니다.

기존의 개발 방식에는 무엇이 문제인지 명확히 정의되지 않은 상태에서 중구난방으로 개발을 진행하게 되는 경항이 있고, 이렇게 될 경우 본질에 집중하기 힘들어 코드가 더러워 질 가능성이 높습니다. 그러나 TDD에서는 문제를 먼저 정의하기 때문에 개발자가 진짜 필요한 부분에 집중할 수 있습니다. 이는 코드를 더 클린하게 만들고 개발 퍼포먼스 향상에도 큰 영향을 끼칩니다.

TMI

현재 제가 다니고 있는 뱅크샐러드에서는 개발 Flow의 첫 부분으로 테스크펙이라는 문서를 작성하고 있습니다. 개발 전에 문제를 먼저 정의하고 코드 구현을 한다는 점에 있어서 어느정도 위와 동일한 효과를 볼 수 있었습니다.

무엇을 어떻게 테스트 해야할까?

테스트 환경

이번에는 위의 제목과 같이 React Testing Library를 사용하여 테스트 환경을 구축하려 합니다.

React Testing Libarary는 Jest 기반으로 만들어 졌으며, 사용자가 컴포넌트를 사용하는 것처럼 테스트를 작성할 수 있습니다.

테스트 범주

테스트 코드는 내가 만든 기능의 주요 핵심이 무엇인지 파악하고, 그 핵심에 집중해 테스트 코드를 작성하는 것이 좋습니다.

예를들면, util 성격의 라이브러리인 lodash가 잘 동작하는지 확인하거나, useEffect({} , [])가 mount 된 직후에 호출되는지는 굳이 테스트 할 필요가 없습니다. 이러한 요소들은 lodash나 react 라이브러리에서 해야할 역할이며, 우리는 lodash를 사용한 메서드나, useEffect() 안의 로직이 잘 동작하는지 확인하면 됩니다.

저는 이러한 관점에서 크게 2가지 부분으로 나누어 테스트 범주를 생각 했습니다.

  1. Business logic
  2. Components

Business logic

비즈니스 로직의 경우 서비스의 규모나 상황에따라 각기 다른 구조로 구현을 하며, 구조별로 더 유용한 테스트 방식이 있을 것이라 생각 됩니다. 해당 글에서는 비즈니스 로직과 관련된 테스트 코드의 이야기는 제외하려 합니다.

TMI

현재 회사에서 실험플랫폼 (A/B Testing Platform)을 구현하고 있는데, 하나의 페이지에서 여러 API를 호출하거나, 페이지 이동 후에도 State를 가져와 활용하는 등의 복잡도가 높은 서비스 입니다. 이러한 상황을 고려하여 redux-toolkitredux-saga 조합을 사용하였는데, 해당 조합을 덕분에 비즈니스 로직의 테스트코드를 매우 쉽고 깔끔하게 작성할 수 있었다.

Components

여기서는 Components에서 Presentational Components에 대해 주요하게 다루려고 합니다. React는 화면을 그려주는 라이브러리이고, 사용자에게 직접적으로 보여지는 부분이기 때문에 중요하게 테스트 되어야한다 생각됩니다.

Presentational Components에서 생길 수 있는 경우들을 생각하고 테스트 케이스를 작성하며, 주로 아래와 같은 것들이 있습니다.

  1. Props가 잘 받아와 졌는가?
  2. State가 의도한 대로 잘 관리되고 있는가?
  3. Props나 State를 토대로 Component가 잘 rendering 되고 있는가?
  4. event handler가 잘 동작 하는가?
  5. lifecycle에 맞게 동작하는가?

TDD 코드 작성하기

0. 시작하기 전

테스트 코드 작성법

기본적으로 react-testing-library에서 컴포넌트를 렌더링 할 때에는 render() 함수를 사용합니다. 해당 함수에 render하고자 하는 component를 넣어주면, DOM을 선택할 수 있는 쿼리들과 container가 반환된다. 그 내부 쿼리 함수로 getByText가 존재하며 해당 함수를 사용하면 Text를 사용하여 원하는 dom을 선택 할 수 있다. 이 이외에도 다양한 쿼리가 존재하며 Cheatsheet를 적극 활용하자.

추가로 describe 메서드를 사용해 무엇을 테스트 할 것인지 기술하는 Test suite를 작성하고, it 메서드로 실제 assertion하려는 테스트 케이스를 작성한다. 그리고 expect를 이용해서 assertion의 결과를 확인한다.

예시 화면

testcode 작성용 예시 화면

TDD 작성 순서를 구현하는데 참고할 만한 예시 화면이며 모든 Flow를 다루진 않습니다.

1. 실패 코드 작성하기

먼저 의도하고자 했던 기능들을 테스트 코드로 먼저 작성합니다..

describe('label이 존재하는 input componrnt', () => {
  it('label props에 "아이디"를 넣으면 label 영역에 아이디가 render 되어야 한다.', () => {
    const LABEL = '아이디';
    const { getByText } = render(<Input label={ LABEL } />);
    
    const labelElement = getByText(new RegExp(LABEL, 'i'));
    
    expect(labelElement).toBeInTheDocument();
  })
  
  .. 그외 필요하다 생각되는 테스트 코드들 추가
})

현재는 구현체가 없기 때문에 아래와 같이 테스트가 실패합니다.

testing fail

2. 성공 코드 작성하기

테스트를 통과할 수 있는 최소한의 코드를 작성합니다.

import React from 'react';

interface Props {
  label: string;
}

export const Input = ({ label }: Props) => (
  <div>
    <label>{ label }</label>
  </div>
);

아래와 같이 성공하는 것을 확인합니다.

testcode success

3. 리팩토링

2번 성공 코드의 경우 테스트 코드를 통과할 수 있는 정말 최소한의 코드만 작성했기에, 상황에 따라서는 비효율 적인 로직이나 의도가 명확하지 않은 코드들이 있을 수 있습니다. 이러한 것들을 해당 step에서 개선해 나가면 됩니다.

(해당 코드의 경우 너무 단순하다보니 리팩토링 할 것이 없어보인다. 😅)

4. 반복

위의 1, 2, 3의 과정을 반복하면서 테스트 코드와 실제 코드들을 작성해 나가면 됩니다.

마무리.

테스트 코드를 미리 작성하고 개발을 하다보니 구현하고자 했던 내용들을 코드 구현 전에 되짚어 보고, 해당 코드의 핵심이 무엇인지 한번 더 생각하게 되었습니다. 또한 터미널에 테스트 코드 결과가 바로바로 나오니 굳이 코드 결과를 브라우저를 통해 확인하지 않아도 된다는 점과 어떤 부분에서 에러가 나는지 브라우저보다 더 정확하게 파악할 수 있다는 점이 좋았습니다.

다만, 아직 이런 방식의 테스트 코드가 익숙치 않기에 구현 후 되돌아보니 불필요한 테스트들이 눈에 보였습니다.(불필요한 곳에서 테스트 커버리지를 높임ㅠ) 정말 의미가 있는 테스트 코드를 작성하는 연습이 필요할 것으로 보여집니다.

참고 자료

🌝동글동글 🌚