Front End/Web

TDD 방법론

TDD(Test-driven Development)

TDD는 코드를 작성하기 전에 테스트를 쓰는 소프트웨어 개발 방법론이다. 개발자 자신이 바람직하다고 생각하는 코드의 결과를 미리 정의하고 이를 바탕으로 코드를 작성하는 방법으로, TDD를 통해 소프트웨어를 개발하는 것은 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 작성하는 과정을 반복하는 것을 의미한다.

TDD의 개발 주기

TDD의 개발 주기를 그림으로 나타내면 위와 같이 총 3단계로 이루어진다.

  1. Write Failing Test : 실패하는 테스트 코드를 먼저 작성한다.
  1. Make Test Pass : 테스트 코드를 성공시키기 위한 실제 코드를 작성한다.
  1. Refactor: 중복 코드 제거, 일반화 등의 리팩토링을 수행한다.

이 과정에서 1의 과정을 마치기 전에 2의 작업을 시작하지 않도록 주의해야 한다. 또한 2를 진행할 때는, 1의 테스트를 통과할 정도의 최소 코드만 작성해야 한다. 테스트를 먼저 작성하는 것은 필연적으로 코드를 어떻게 구성할지 고민하게 된다는 것을 의미하고, 결과적으로 버그가 더 적은 코드를 짤 수 있게 된다. 또 불필요한 설계를 피할 수 있고, 테스트 코드의 요구 사항에 집중할 수 있게 된다. 일반적으로 TDD를 따라 소프트웨어를 개발할 경우 그렇지 않은 경우보다 결함을 50~90%까지 감소시킬 수 있다.

버그가 적은, 보다 효과적인 코드를 짤 수 있는 방법이지만 실제로 완전한 TDD를 따르는 개발자는 이외로 많지 않다. 그 이유는 대부분의 개발자들이 생각하고 일하는 방식과 일치하지 않기 때문이다. 대부분의 개발자는 테스트를 작성하는 것 보다, 만들어야 하는 것을 바로 코드로 작성하는 방식이 훨씬 자연스럽고 빠르다고 느낄 것이다. 많은 개발자들에게 왜 TDD를 따르지 않는지 물어보면, 대부분 ‘속도’ 때문이라고 대답할 것이다.

TDD를 사용하는 이유

코드를 작성하기 전에 테스트 코드를 먼저 작성하는 것이 시간이 오래 걸리는 것 처럼 느껴지지만, 오히려 예상하지 못했던 버그를 줄여 소요 시간을 줄일 수 있기 때문이다. 개발 과정에서 코드는 다양한 조건에 의해 계속 삽입/수정/삭제 되는데, 이 과정에서 코드가 중복되거나 불필요한 코드가 남게 된다. 그리고 이로 인해 여러 버그가 발생하거나, 디버깅 또한 어려워지기도 한다. 결국 그런 코드를 유지보스하기 위해서 처음 개발할 때 아꼈던 리소스보다 더 많은 리소스를 투입해야 하는 경우가 발생한다.

당장 완전한 TDD를 따르는 것은 아주 어려운 일이지만, 작성하려는 코드에 대해 특정한 규칙(테스트)를 설정하기 위해 고민하면서 코드가 큰 틀에서 어떤 의미를 갖는지 고민하는 것은 개발자로서 성장하는데 중요한 도구가 될 수 있다.

테스트 코드를 작성하는 방법

console.log를 통해 현재 작성중인 코드가 어떤 결과물을 도출하는지 확인하는 것도 일종의 테스트이다. 여러 개발자들이 더 나은 테스트를 작성하기 위해 많은 테스트 오픈소스 프레임워크를 제작했으며, 대표적으로는 mocha라는 테스트 프레임워크와 chai라는 라이브러리를 사용한다.

React 환경에서 테스트하기

React에서 테스트는 Testing Library, Jest를 이용할 수 있다. Testing Libarary에서 React용 React Testing Library을 제공하고 있기 때문에, create-react-app을 이용하여 React 프로젝트를 생성하면 자동으로 Testing Libarary를 이용할 수 있으며, 이렇게 설치한 Testing Library는 실행하고 싶은 컴포넌트나 클릭 이벤트 등 다양한 곳에 쓸 수 있다.

Jest는 JavaScript의 Testing Framework / Test Runner로 테스트 파일을 자동으로 찾아 테스트를 실행하고, 테스트를 실행한 결과 기대만큼 올바른 값을 가지고 있는지 함수를 이용하여 체크해 테스트가 성공인지 실패인지 판단해준다.

Testing Libarary와 Jest는 역할이 각각 다르기 때문에 React에 대한 테스트를 진행할 때는 어느 한쪽 라이브러리를 이용하는 것만으로는 테스트를 할 수 없다.

React 기본 테스트 환경 확인

  1. 새로운 React 프로젝트 생성하기

    CRA(npx create-react-app)를 통해 React 프로젝트를 생성하면 테스트 환경이 설정되어 바로 테스트가 가능하다. 프로젝트 생성 후 폴더로 이동하여 package.json 파일을 확인하면 dependencies 안에 @testing이라는 접두어가 붙은 3개의 라이브러리를 확인할 수 있는데, 이것들이 바로 테스트를 수행할 때 이용하는 라이브러리이다. scripts안에 test가 있기 때문에, 테스트를 실행할 때는 npm run test를 이용하면 된다.

    package.json 파일
    테스트를 위한 코드
    CRA로 React 프로젝트 설치 시 기본으로 설치되는 테스트 모듈
    1. @testing-library/jest-dom : Jest-dom이 제공하는 custom matcher를 사용할 수 있게 해준다.
    1. @testing-library/react : 컴포넌트의 요소를 찾기 위한 query가 포함되어 있다.
    1. @testing-library/user-event : click 등 사용자 이벤트에 이용된다.
  1. 테스트 파일 확인하기

    src폴더 안을 보면, setupTests.jsApp.test.js라는 이름의 파일을 확인할 수 있다. App.test.js에는 간단한 테스트가 이미 만들어져 있고, 작성한 프로젝트의 package.json파일과 파일의 구성을 보면 CRA 명령으로 프로젝트를 생성함으로써 테스트를 하기 위한 환경이 사전에 설치되어 있는 것을 알 수 있다. npm run test 명령어로 test를 실행하면 아래와 같은 화면이 나온다.

    실행을 하면 대화식 메시지가 나타나는데, 메시지를 보면 키보드 키에 따라 다양한 처리를 할 수 있다는 것을 알 수 있다. Press a to run all tests. 라고 설명되어 있는 부분을 실행하기 위해 a키를 눌러 보면 아래의 화면처럼 테스트가 실행되고 실행한 테스트에 성공했음을 의미하는 PASS 메세지와 테스트 건수나 테스트에 걸린 시간 등도 확인할 수 있다.

    App.test.js에는 Jest 함수로 테스트를 실행할 때 반드시 이용하는 함수인 test 함수가 작성되어 있다. test 함수의 첫 번째 인자는 테스트가 어떤 내용인지 나중에 다시 읽어도 테스트 내용을 알 수 있는 설명을 작성하고, 두 번째 인자는 하고자 하는 테스트를 함수의 형태로 넣는다.

    전달인자로 들어간 함수를 분석해보면 아래와 같다.

    1. 첫번째 줄은 테스트하고자 하는 컴포넌트를 render()함수로 전달하고 있다. eact-testing-library에서는 테스트를 진행할 컴포넌트를 render()함수의 인자로 전달한다.
    1. 두번째 줄은 screen의 다양한 메서드 중 getByText()메서드를 이용해 "learn react"라는 문자열이 있는지 확인하여 linkElement에 할당하고 있다. (여기서 i는 정규식 표현으로 i를 붙임으로써 대소문자를 구분하지 않게 만들어준다)
    1. 세번째 줄은 expect함수의 인자로 지정한 요소가 document.body에 존재하는지 toBeInTheDocument함수를 사용하여 체크하고 있다. 여기서 toBeInTheDocument 함수는 matchers 함수라고 부른다.

    App.test.js 파일 중 이용되고 있는 test 함수, expect 함수는 Jest의 함수고, toBeInTheDocument는 jest-dom 라이브러리에 포함된 Custom matchers이다. jest-dom은 src폴더의 setupTests.js파일 내에서 import 되고 있다. 이 파일에서 import를 삭제하면 toBeInTheDocument함수를 이용할 수 없다.

    테스트로 확인한 부분 App.js 파일을 보면 <a>태그의 콘텐츠로 "Learn React"를 찾을 수 있는데, App.test.js의 내부에서 screen의 getByText 메서드로 찾아낸 부분이 바로 이 부분이다. 만약 App.js에서 Learn React가 일부 변경이 된다면 테스트는 FAIL로 뜨고 실패하게 될것이다.

  1. 간단한 테스트 직접 만들기

    React를 이용해 테스트를 실행하는 것은 처음에 어렵게 느껴질 수 있기 때문에, Jest만을 이용해 테스트를 실행하고 테스트의 기본적인 규칙과 기술 방법을 확인해보자. 아까 보았던 것 처럼 테스트는 test함수 안에 쓰고 첫번째 인자에는 테스트에 대한 설명, 두 번째 인자에는 테스트 내용을 함수의 형태로 작성한다.

    expect 함수의 인자로 2+2를 넣고 toBe함수에 4라는 결과를 지정하여 테스트를 해보면 통과하는 것을 확인할 수 있다. toBe 함수는 matchers 함수 중 하나로 expect 함수에 지정한 값이 toBe함수에 지정한 값과 일치하는지 체크한다. 만약 toBe의 전달인자로 5를 넣으면 테스트는 통과하지 못한다.

    테스트 함수 대신 it을 사용해도 같은 결과가 나오고, describe함수를 사용하면 it이나 test함수를 하나의 파일에 여러개 포함할 수 있다. describe는 Test Suites라고 불리며 test/it함수 블록은 Test(Test case)라고 한다.

직접 만든 컴포넌트 테스트하기

  1. 컴포넌트 만들기

    전원의 상태를 OFF에서 ON으로 전환하는 심플한 컴포넌트, Light.jsx를 생성하고 App.js에 import한다. 이 컴포넌트는 name을 prop으로 전달받는다.

    import { useState } from "react";
    
    function Light({ name }) {
      const [light, setLight] = useState(false);
    
      return (
        <div>
          <h1>
            {name} {light ? "ON" : "OFF"}{" "}
          </h1>
          <button onClick={() => setLight(true)} disabled={light ? true : false}>
            ON
          </button>
          <button onClick={() => setLight(false)} disabled={!light ? true : false}>
            OFF
          </button>
        </div>
      );
    }
    
    export default Light;
    import Light from "./components/Light";
    import "./styles.css";
    
    export default function App() {
      return (
        <div className="App">
          <Light name="전원" />
        </div>
      );
    }
  1. 컴포넌트 테스트 하기

    Light.test.js 파일을 만들고 아래와 같이 테스트를 작성한다. name을 props로 전달받아 전원이 올바르게 표시되어있는지 확인하는 테스트 코드이다. 올바르게 작성되었다면 PASS가 나오게 될 것이다.

    import { render, screen } from '@testing-library/react';
    import Light from './Light';
    
    it('renders Light Component', () => {
    	render(<Light name="전원" />);
    	const nameElement = screen.getByText(/전원 off/i);
    	expect(nameElement).toBeInTheDocument();
    })

    다음은 OFF 버튼이 disabled로 되어있는지 matchers 함수의 toBeDisabled 함수를 이용해 테스트를 작성한다. getByRole을 이용하여 button을 지정했고, 버튼이 2개이므로 옵션의 name 을 이용하여 OFF 버튼을 찾는다. 현재는 OFF 버튼이 disabled 상태이기 때문에 테스트 역시 통과하게 된다. 만약 disabled가 아니라는 것을 테스트하려면 toBeDisabled 함수 앞에 not을 붙이면 구현할 수 있다.

    it('off button disabled', () => {
    	render(<Light name="전원" />);
    	const offButtonElement = screen.getByRole('button', { name: 'OFF' });
    	expect(offButtonElement).toBeDisabled();
    })
    
    // disabled 상태가 아닌 것을 확인하는 테스트
    it('on button enable', () => {
      render(<Light name="전원" />);
      const onButtonElement = screen.getByRole('button', { name: 'ON' });
      expect(onButtonElement).not.toBeDisabled();
    });

    버튼 클릭의 이벤트 유무도 테스트로 구현 가능하다. fireEvent를 import하고, fireEventclick메서드에 전달인자로 테스트하고자 하는 요소를 전달하면 된다.

    // 이벤트 후의 ON의 disabled 상태를 확인하는 테스트
    it('change from off to on', () => {
    	render(<Light name="전원" />);
      const onButtonElement = screen.getByRole('button', { name: 'ON' });
      // fireEvent는 테스트 환경에서 클릭 이벤트를 구현한다.
    	fireEvent.click(onButtonElement);
    	expect(onButtonElement).toBeDisabled();
    })

CodeSandbox


Uploaded by N2T