본문 바로가기

Dev.

TDD 는 코드 설계를 위한 도구이다.

[출처] - pixels

저는 언제부터인가 '어떻게 하면 코드의 품질을 좋게하고, 서비스 안정성을 높일 수 있을까?' 라는 고민을 가지게 되었습니다. 여러 방법중 TDD라는 개발 방법론에 관심을 가지게 되었고, TDD를 공부하게 되었습니다. TDD에 대한 회의적인 시각이 많았는데요. 저는 오히려 TDD를 긍정적으로 보는 입장입니다. 왜 그런지 이번 글을 통해 정리해보겠습니다.

 

 

목차

  • 들어가기 전에
  • TDD가 테스트를 위한 거라고?
  • 왜 좋은 코드가 만들어 지는데요?
  • 그래! TDD 한번 해보자!
  • 우리가 얻은 이점!
  • 테스트 커버리지 100%?
  • 마무리
 

 


들어가기 전에

이 글에서는 범위를 2가지로 좁혀보겠습니다.

프론트에 대한 TDD

우선 프론트단에서의 TDD를 이야기 할겁니다. 그 중 가장 인기 있는 React 를 기반으로 한 예제를 다룰 것이고, 그렇다 보니 함수지향형 패러다임을 기반으로 이야기 될 겁니다.

단위테스트

테스트도 종류가 나뉩니다. 우리는 그 중에서 단위 테스트에 대해 초점을 맞출 겁니다.

 

 

TDD가 테스트를 위한 거라고?

TDD에 대해 이야기 하면, 테스트 코드에 대한 대화주제로 빠지는 경우가 많습니다. TDD의 뜻풀이가 Test Driven Development 이기 때문에 무리도 아닙니다. 하지만 TDD는 좋은 설계를 하기 위한 방법입니다. 테스트를 진행하기 위해서는 테스트 친화적인 코드를 작성해야 합니다. 그런 코드를 만들려고 하다보면, 결국 좋은 코드가 설계 됩니다.

'기술부채 잡는 품격있는 코드'[링크] 에서도 말씀드렸듯이 TDD를 통해서 작성되는 테스트 코드는 덤인 겁니다.

 


왜 좋은 코드가 만들어 지는데요?

테스트가 유리한 코드는 어떤 코드일까요?

1. 입력값 => 출력값이 충족되는 함수

잠깐 함수형 프로그래밍에 대해 잠깐 언급해 보겠습니다. '함수형 코딩' 이라는 책에서는 함수형을 3가지 분류로 나누고 있습니다.

💥 액션

액션은 코드의 실행 시점이나 횟수에 의존합니다. 그리고 액션을 포함하는 코드는 액션이 됩니다. 즉 액션은 번집니다.

계산

계산은 입력값으로 출력값을 내뱉습니다. 같은 입력값을 넣으면 항상 같은 출력값을 내뱉습니다.

📡  데이터

데이터는 데이터 그 자체입니다.

여기서 가장 테스트하기 좋은 코드는 계산입니다. 액션과는 달리 같은 입력값일 때 항상 같은 결과값을 내뱉습니다. 테스트를 쉽게 하기 위해서는 이런 계산 형태의 함수가 유리합니다.

테스트를 위해 계산 형태의 코드를 작성하려고 노력하다보면, 값의 불변성이 보장되는 코드가 만들어집니다.

2. 단일책임 원칙

객체지향의 5대 원칙중 하나인 단일책임 원칙(SRP)은 객체에만 국한되지 않습니다. 하나의 함수도 단일책임 원칙을 지켜주는 것이 중요합니다. 응용해보면, (5대원칙은 '객체지향의 5대 원칙, 이것만을 알고 가자 (준비중)' 에서 더 자세한 내용을 볼수 있습니다)

하나의 함수는 하나의 동작만의 책임을 갖는다

 

테스트 코드를 빠르게 작성하기 위해서는 내가 구현하고자 하는 기능이 복잡하면 테스트 경우의 수를 생각해내기가 쉽지 않습니다. 테스트 코드가 간략해질려면, 구현하고자 하는 코드가 단순해야합니다. 그래야 생각하기 쉽죠!. 코드를 더 잘게 쪼개어서 생각하게 되고 단일책임 원칙이 지켜진 코드를 작성하게 됩니다.

3. 독립성

각각의 기능은 독립적인 것이 좋습니다. 내가 테스트하고자 하는 코드가 다른 상태에 의해 의존하게 된다면, 일단 그 코드는 계산이 아닌 액션 코드입니다. 그리고 그런 코드는 테스트 하기 힘듭니다. 실행 시점 또는 횟수 등등을 만족시켜줘야 할 상황이 전제되어 있습니다. 그걸 모두 충족시키기가 여간 골치 아픈게 아닙니다.

그래서 테스트를 작성할 때, 코드의 의존성을 없애고자 노력하게 됩니다. 안 그러면 테스트 코드부터가 작성이 안되니깐요.

결국 테스트코드를 쉽게 작성하기 위해서였지만 자연스럽게 불변성이 보장되는 코드, 단일책임 원칙, 독립적인 코드 등등이 지켜지는 코드가 완성됩니다.

 

 


그래! TDD 한번 해보자!

TDD통해서 좋은 코드 설계가 자연스럽게 된다는건 이제 알았습니다. 프론트 단 React 에서는 유닛테스트 차원에서

View 영역 ➡ React testing library, enzyme
Business ➡ Jest, testing library hooks

등등 이 있습니다.

그 외 테스트를 돕는 라이브러리는 더 있지만, 차차 알아가보도록 하겠습니다.

 

간단한 예제는 버튼을 눌렀을 시 현재의 날짜를 YYYY.MM.DD 포멧으로 화면에 보여주는 기능을 구현해 볼겁니다. 예제 코드를 포함하면 글이 길어지니까 글을 나눠서 작성해볼게요. 우선 이 글에서는 View 단부터 작성해봅시다.

 

우리는 아래 과정을 반복할 겁니다.

실패하는 테스트 작성테스트 코드 통과시키기코드 개선하기

 

 

🔴 App 실패하는 테스트 작성

View 틀만 만들어 놓고,

App.tsx

import React from 'react';

const App: React.FC = () => {
	return null;
}
export default App;

 

​View를 테스트하는 테스트 코드를 작성합니다.

App.test.tsx

import {render, screen} from '@testing-library/react'
import App from './App'

describe('App', () => {
  it('renders the title and button', () => {
    const { getByText } = render(<App />);
    const title = getByText(/Hello Short/i);
    const button = getByText(/Click!/i);

    expect(title).toBeInTheDocument();
    expect(button).toBeInTheDocument();
  });
});

 

당연히 에러가 납니다. App.tsx 에는 아무것도 없으니깐요.

테스트 상황 in 콘솔

 

🟢 App 테스트 코드를 통과시키기

이제 View단을 아래와 같이 작성하면,

App.tsx

import React from 'react';

function App() {
  return (
    <div>
      <h1>Hello Short</h1>
      <button>Click!</button>
    </div>
  );
}

export default App;

이렇게 테스트가 만족되고, 성공 표시가 뜹니다.

테스트상황 in 콘솔

🔵 App 코드 개선 시키기

이젠 click 이벤트도 테스트를 붙이고 싶습니다. 자 여기서 문제가 생깁니다. 어떻게 테스트를 작성해야할까요?

onClick 은 함수를 주입해야합니다. 클릭이 일어날 시 함수가 호출되죠. 우린 그 호출되는 함수를 mocking 해야합니다. 🤔

App.tsx

const click = () => {
  // click 함수가 호출 되는지 테스트 하려면,
  // 테스트 환경에서는 click 을 mock 함수로 만들어서 테스트 해야 합니다.
}

...

<button onClick={click} >버튼</button>

 

그런데,

React는 functional components 로 구현하는 것이 규격이 되었습니다. functional은 참조가 불가능합니다. 클래스 컴포넌트처럼 참조를 통해 onClick 은 불가능하다는거죠. 아래와 같이요.

클래스 컴포넌트 click 이벤트 테스트코드

const component = new App();

expect(component.onClick()).toBeCalled();
 

그래서 onClick 을 외부에서 넘겨주게 되면 onClick 테스트가 가능해집니다. 그리고 테스트 할 땐, 그 onClick을 mocking 하는거죠. 아래와 같이요.

함수형 컴포넌트 click 이벤트 테스트코드

const mockedClick = jest.fn();

const screen = render(<App onClick={mockedClick) />);
const button = screen.getByText('확인버튼');

fireEvent.press(button);

expect(mockedClick).toBeCalled();

 

여기서 우리는 컴포넌트의 분리를 생각하게 됩니다. 🔪

Button 컴포넌트를 따로 만들고 거기에 props를 넘겨주는 방식으로요. 왜요? 테스트를 위해서요!

 

 

🔴 Button 실패하는 테스트 작성

Button 컴포넌트 테스트를 짜보죠.

Button.test.tsx

import React from 'react';
import { render, fireEvent } from "@testing-library/react";
import Button from './Button';

describe('Button', () => {
  // 아래와 같으 jest.fn() 으로 mock 함수를 만들고
  // 컴포넌트에 props 로 넘겨줍니다.
  // 이제 onClick 에 대한 호출을 테스트 할 수 있습니다.
  const onClick = jest.fn();

  function renderButton() {
    return render(<Button onClick={onClick} />);
  }

  it('click the button', () => {
    const { getByText } = renderButton();
    const button = getByText(/Click!/i);

    fireEvent.click(button);

    expect(button).toBeInTheDocument();
    expect(onClick).toBeCalled();
  })
});

테스트를 돌려보면 당연히 에러가 나죠.

테스트 상황 in 콘솔

 

🟢 Button 테스트 코드를 통과시키기

그리고 테스트 에서 표현한 대로 구현을 완료하면,

Button.tsx

import React from 'react'

const Button: React.FC<Props> = (props) => {
    const { onClick } = props
    return <button onClick={onClick}>Click!</button>
}
export default Button

type Props = {
    onClick: () => void
}

 

또다시 테스트를 통과했습니다.

테스트 상황 in 콘솔

 

🔵 Button 코드 적용 시키기

이제 구현한 Button을 App 에 적용할게요.

App.tsx

import React from 'react';
import Button from './Button';

function App() {
  const onClick = () => {
    console.log('clicked');
  }
  return (
    <div>
      <h1>Hello Short</h1>
      <Button onClick={onClick}/>
    </div>
  );
}

export default App;

 

짜잔! 우선 View 단에서의 구축은 완료되었습니다. 이제 비즈니스 로직을 구현해야하는데요, 이야기가 길어질것 같으니 ' React hooks 를 TDD로 작성해보기 (준비중)' 에서 더 이어갈게요.


 

우리가 얻은 이점!

예제가 쉬워서 별것 아닌거 같지만 우리는 TDD를 통해 2가지 효과를 얻었습니다.

컴포넌트의 역할분리를 자연스럽게 해냈습니다.

우린 테스트 코드에 친화적으로 만들고자, 컴포넌트를 분리했습니다. 하지만 결과적으론 단일책임원칙을 만족하는 컴포넌트를 만들어냈어요.

비즈니스 로직을 한 곳으로 몰았습니다.

Button 컴포넌트에서는 onClick 을 단순히 넘겨받을 뿐입니다. 이제 App 에서 주요로직을 구현하면 되는거죠. Button은 그냥 Rending만 하면 됩니다. 앞으로 App 에 주요 로직을 몰아주면 되겠죠?

 

우린 주요 로직을 수정하거나 더해갈때 App 컴포넌트만 보면 됩니다. 한마디로 주요 로직에 대한 응집도를 높였습니다. 🎁

이런 이 점은 앱이 커질 수록 더 극대화 됩니다. TDD를 통해 설계단 부터 코드에 대한 많은 생각을 하게 되죠.

'뭐가 유리할까?', '어떻게 넘기지?', '어떤 경우들이 발생할까?' 등등을 충분히 고려하고 본 코드를 작성하게 됩니다. 그 품질이 일반적인 경우보다 확연히 차이가 나죠

 

 

 

테스트 커버리지 100%??

더 나아가 우리는 테스트 커버리지를 통해 테스트가 얼마나 촘촘히 체크하고 있는지 확인할 수 있습니다. 100% 라는 수치는 모든 영역을 테스트 코드로 커버했다는 의미겠죠.

하지만 이런 수치가 의미 있을까요? 그렇지 않습니다. 계속 강조하지만 우린 TDD가 테스트 코드 구축을 위한 수단이 아니라 코드 설계를 위한 수단으로 사용하고 있습니다.

 

커버리지가 100%가 아니라도 🏆 좋은 설계를 이미 완료했다면, 테스트 코드를 굳이 작성하지 않아도 된다고 봅니다. 그 이상은 오버스펙인거죠! 우리가 좋은 설계구조를 가져가려고 노력하는 이유는 생산성을 지속적으로 유지하기 위해서입니다. 기술 부채를 계속 관리하기 위해서이죠.

 


 

마무리

사실 TDD를 습득하는 건 쉽지 않습니다. 꽤 시간이 걸리는 스킬입니다. 특히 프론트 영역에서는 이에 대한 논의가 벡엔드에 비해 여전히 회의적인 시각이 많습니다. 그도 그럴것이 화면에 대한 정의가 자주 바뀌고, 이벤트성 화면도 많습니다. 화면단의 코드를 맡고 있는 프론트의 입장에서는 이런 상황에서 TDD라니.. 무리도 아닙니다.

그렇기에 적절한 선택과 집중은 필요합니다. TDD를 통해 얻는 이점은 많습니다. 좋은게 좋은거죠! 다만 우리에게는 일정이라는 기한이 있습니다. 일정내에 가장 효율적으로 설계를 하는 것이 정답이겠죠.

그리고 하다보면 테스트를 할 수 있는 것이 있고, 없는 것이 있습니다. 하기 힘든 것에 대해서는 적절한 타협도 필요합니다.

하지만 이 글을 통해 말씀드렸듯이 TDD는 좋은 설계를 위한 도구라고 봐야합니다. 한번 쓰이고 버려질 코드가 아니라면 코드 설계에 공을 드리는 것이 맞습니다. TDD라는 기법을 Test 코드 구축의 일환으로 보는 것은 출발부터가 잘못 되었다고 봅니다.

 
 
 

 


아래는 개발 커뮤니티 링크입니다. 들어오셔서 기술 정보, 트렌드 정보, 유료스터디, 무료스터디, 모각코 등등을 이용해 보세요.