dev.toMatti Bar-Zeev](https://dev.to/mbarzeev) 님이 작성한 Creating a React component with TDD 번역 자료입니다. 번역에 문제가 있다면 댓글 달아주시구요. 원문을 보시기를 추천드립니다

테스트 주도 개발(TDD) 접근 방식을 사용하여 React 컴포넌트를 만든다면 이 포스트와 함께하시죠.

다음과 같은 기능이 있는 확인(confirmation) 컴포넌트를 만들 것입니다.

  • 정적 타이틀
  • 확인 질문 (question)
  • 외부 핸들러를 지원하는 확인(OK) 버튼
  • 위부 핸들러를 지원하는 취소(Cancel) 버튼

두 버튼 모두 컴포넌트의 책임에서 벗어났기 때문에 클릭 할 때 어떤 일이 일어나는지 알지 못하지만 컴포넌트는 버튼에 대한 콜백을 제공하기 위해 사용하는 다른 컴포넌트/컨테이너를 활성화해야 합니다.

다음과 같이 표시됩니다.

Image description

시작하겠습니다.

TDD의 과정은 테스트 작성 주기 => 실패 관찰 => 통과를 위한 최소 코드 작성 => 성공 관찰 => 리팩터링(필요한 경우) => 반복, 이것을 제가 여기서 연습할 것입니다.

어떤 시점에는 지루하거나 비현실적으로 보일 수 있지만 이 일을 원칙대로 할 것을 주장하고 그것이 당신의 목적에 적합한지 도중에 시간을 줄이고 싶은지 결정하는 것은 당신에게 맡기고 싶습니다. 먼저 테스트 파일로 이동합니다. watch 모드로 Jest Testing env를 실행하고 “Confirmation”이라는 컴포넌트의 디렉터리와 그 안에 “index.test.js” 파일을 만들었습니다.

첫 번째 테스트는 매우 추상적입니다. 컴포넌트가 존재하는지 확인하기 위해 컴포넌트를 렌더링할 때 무언가(무엇이든)가 렌더링되는지 확인하려고 합니다. 실제로 (아직 존재하지 않는) 컴포넌트를 렌더링하여 “dialog”의 역할로 찾을 수 있는지 확인합니다. :

import React from 'react';
import {render} from '@testing-library/react';

describe('Confirmation component', () => {
   it('should render', () => {
       const {getByRole} = render(<Confirmation />);
       expect(getByRole('dialog')).toBeInTheDocument();
   });
});

흠, 여러분도 짐작하셨을 것입니다 - Jest는 “확인(Confirmation)”이 무엇인지 모르고 그것은 옳습니다. 이 테스트를 만족할 만큼만 해당 컴포넌트를 생성해 보겠습니다. :

import React from 'react';

const Confirmation = () => {
   return <div role="dialog"></div>;
};

export default Confirmation;

이 컴포넌트를 test로 가져왔더니(import) 통과했습니다. 잘됐네요.

다음으로 이 컴포넌트의 타이틀을 알고 싶습니다. 이 연습의 목적을 위해 타이틀은 정적이며 “확인(Confirmation)”이라고 표시되어야 합니다. 이에 대한 테스트를 만들어 보겠습니다. :

it('should have a title saying "Confirmation"', () => {
	const {getByText} = render(<Confirmation />);
	expect(getByText('Confirmation')).toBeInTheDocument();
});

테스트가 실패했습니다. 이제 통과하도록 코드를 작성합니다. :

import React from 'react';

const Confirmation = () => {
   return (
       <div role="dialog">
           <h1>Confirmation</h1>
       </div>
   );
};

export default Confirmation;

다음 기능으로 넘어가서 이 컴포넌트에 확인 질문(question)이 있는지 확인합니다. 나는 이 질문(question)이 동적이기 때문에 컴포넌트 외부에서 제공되기를 원하며 Confirmation 컴포넌트의 “자식(children)”으로 질문(question)을 갖는 것이 맞다고 생각합니다. 따라서 이에 대한 테스트는 다음과 같습니다. :

it('should have a dynamic confirmation question', () => {
	const question = 'Do you confirm?';
	const {getByText} = render(<Confirmation>{question}</Confirmation>);
	expect(getByText(question)).toBeInTheDocument();
});

또 테스트가 실패하므로 통과하도록 코드를 작성합니다. :

import React from 'react';

const Confirmation = ({children}) => {
   return (
       <div role="dialog">
           <h1>Confirmation</h1>
           <div>{children}</div>
       </div>
   );
};

export default Confirmation;

버튼을 켭시다. 확인(confirm) 버튼부터 시작하겠습니다. 먼저 컴포넌트에 “OK”라고 표시된 버튼이 있는지 확인하고 싶습니다. 이제부터 테스트를 먼저 작성하고 이를 만족하는 코드를 다음과 같이 작성합니다.

테스트:

it('should have an "OK" button', () => {
	const {getByRole} = render(<Confirmation />);
  expect(getByRole('button', {name: 'OK'})).toBeInTheDocument();
});

이 컴포넌트에 적어도 하나의 버튼이 더 있을 것이라는 것을 알고 있기 때문에 여기에서 “이름(name)” 옵션을 사용하고 있습니다.

컴포넌트:

import React from 'react';

const Confirmation = ({children}) => {
   return (
       <div role="dialog">
           <h1>Confirmation</h1>
           <div>{children}</div>
           <button>OK</button>
       </div>
   );
};

export default Confirmation;

“Cancel” 버튼에 대해서도 동일한 작업을 수행해 보겠습니다.

테스트:

it('should have an "Cancel" button', () => {
	const {getByRole} = render(<Confirmation />);
	expect(getByRole('button', {name: 'Cancel'})).toBeInTheDocument();
});

컴포넌트:

import React from 'react';

const Confirmation = ({children}) => {
   return (
       <div role="dialog">
           <h1>Confirmation</h1>
           <div>{children}</div>
           <button>OK</button>
           <button>Cancel</button>
       </div>
   );
};

export default Confirmation;

OK, 좋아요.

우리는 원하는 것을 렌더링하는 컴포넌트를 얻었습니다(스타일이 지정되지 않았지만 그건 또 다른 이야기입니다). 이제 이 컴포넌트의 버튼 핸들러를 외부에서 전달할 수 있는지와 버튼을 클릭할 때 호출되는지 확인해볼 차례입니다.

“OK” 버튼에 대한 테스트부터 시작하겠습니다. :

it('should be able to receive a handler for the "OK" button and execute it upon click', () => {
	const onConfirmationHandler = jest.fn();
	const {getByRole} = render(<Confirmation onConfirmation={onConfirmationHandler} />);
	const okButton = getByRole('button', {name: 'OK'});

	fireEvent.click(okButton);

	expect(onConfirmationHandler).toHaveBeenCalled();
});

제가 한 일은 스파이 함수를 만들어 컴포넌트에 “onConfirmation” 핸들러를 제공하고 “OK” 버튼의 클릭을 시뮬레이션하고 스파이가 호출되었음을 확인하는 것이었습니다.

이 테스트는 분명히 실패하는데, 여기에 이 테스트를 만족시키기 위한 코드가 있습니다.

import React from 'react';

const Confirmation = ({children, onConfirmation}) => {
   return (
       <div role="dialog">
           <h1>Confirmation</h1>
           <div>{children}</div>
           <button onClick={onConfirmation}>
               OK
           </button>
           <button>Cancel</button>
       </div>
   );
};

export default Confirmation;

좋아요, “Cancel” 버튼에 대해서도 동일한 작업을 수행해 보겠습니다. :

테스트:

it('should be able to receive a handler for the "Cancel" button and execute it upon click', () => {
	const onCancellationHandler = jest.fn();
	const {getByRole} = render(<Confirmation onCancellation={onCancellationHandler} />);
	const cancelButton = getByRole('button', {name: 'Cancel'});

	fireEvent.click(cancelButton);

	expect(onCancellationHandler).toHaveBeenCalled();
});

컴포넌트:

import React from 'react';

const Confirmation = ({children, onConfirmation, onCancellation}) => {
   return (
       <div role="dialog">
           <h1>Confirmation</h1>
           <div>{children}</div>
           <button onClick={onConfirmation}>
               OK
           </button>
           <button onClick={onCancellation}>
               Cancel
           </button>
       </div>
   );
};

export default Confirmation;

다음은 전체 테스트 파일입니다. :

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

describe('Confirmation component', () => {
   it('should render', () => {
       const {getByRole} = render(<Confirmation />);
       expect(getByRole('dialog')).toBeInTheDocument();
   });

   it('should have a title saying "Confirmation"', () => {
       const {getByText} = render(<Confirmation />);
       expect(getByText('Confirmation')).toBeInTheDocument();
   });

   it('should have a dynamic confirmation question', () => {
       const question = 'Do you confirm?';
       const {getByText} = render(<Confirmation>{question}</Confirmation>);
       expect(getByText(question)).toBeInTheDocument();
   });

   it('should have an "OK" button', () => {
       const {getByRole} = render(<Confirmation />);
       expect(getByRole('button', {name: 'OK'})).toBeInTheDocument();
   });

   it('should have an "Cancel" button', () => {
       const {getByRole} = render(<Confirmation />);
       expect(getByRole('button', {name: 'Cancel'})).toBeInTheDocument();
   });

   it('should be able to receive a handler for the "OK" button and execute it upon click', () => {
       const onConfirmationHandler = jest.fn();
       const {getByRole} = render(<Confirmation onConfirmation={onConfirmationHandler} />);
       const okButton = getByRole('button', {name: 'OK'});

       fireEvent.click(okButton);

       expect(onConfirmationHandler).toHaveBeenCalled();
   });

   it('should be able to receive a handler for the "Cancel" button and execute it upon click', () => {
       const onCancellationHandler = jest.fn();
       const {getByRole} = render(<Confirmation onCancellation={onCancellationHandler} />);
       const cancelButton = getByRole('button', {name: 'Cancel'});

       fireEvent.click(cancelButton);

       expect(onCancellationHandler).toHaveBeenCalled();
   });
});

그리고 이게 다라고 생각해요! 우리는 컴포넌트의 모든 컴포넌트와 로직을 구현하고 완전히 테스트했습니다.

Image description

네, 알아요. 스타일은 꺼져있지만 우리의 빌딩 블록은 손상되지 않고 스펙에 따라 모든 것이 작동한다는 것을 확인한 후에 고칠 수 있어요.

TDD를 사용하여 이 컴포넌트를 만드는 데 나와 함께 한것 외에도 이 포스트는 UI 컴포넌트 개발에 TDD를 적용할 수 있으며 오히려 쉽게 적용할 수 있다는 분명한 증거입니다. TDD는 컴포넌트 기능 스펙을 단계별로 가이드하고 향후 리팩토링을 위한 안전망을 제공하면서 중요한 사항에 집중할 수 있도록 도와줍니다. 이것은 정말 굉장합니다!

항상 그렇듯이, 만약 여러분이 이것을 더 좋게 만드는 방법이나 다른 기술을 가지고 있다면, 반드시 우리와 공유해주세요!

Cheers

이봐! 방금 읽은 내용이 마음에 든다면 Twitter에서 @mattibarzeev를 확인하세요 🍻