[react]5️⃣ TDD 방법으로 todolist-TodoApp

Grace·2021년 5월 5일
0

react-testing-library

목록 보기
5/5

TodoApp 만들기

Todolist의 마지막 작업인 TodoApp 작업은
앞에서 따로 진행했던 유닛테스트를 거친 컴포넌트를 사용해서 구현하기 때문에
일종의 최종점검을 하는 통합테스트 작업이라고 볼 수 있다.

이 컴포넌트에서는 todos 배열에 대한 모든 상태관리를 담당한다.

컴포넌트 렌더링 확인하기

우선 TodoApp컴포넌트의 틀을 잡아준 다음,
그 안에 품어줄 컴포넌트들을 제대로 불러오는지 확인을 해주기 위해
TodoItemList에 data-testid를 설정해준다.

🤔 근거 없는 나의 뇌피셜입니다
TodoItemList에만 testid를 설정해주는 이유는 ❓
TodoApp의 테스트케이스에서 컴포넌트들의 존재유무를 확인해야 하는데, TodoItemList에는 별다른 항목이 없기 때문에
getByTestId로 찾기 위해 따로 설정을 해주는 듯 하다.
➡︎ querySelector로 찾을수도 있지만, 공식문서에서는 DOM API로 선택하기보단 data-testid를 사용하기를 권장한다.

TodoItemList.js

<ul data-testid="TodoItemList">
...
</ul>

이렇게 testid를 설정해주었다면 TodoApp의 테스트케이스를 작성해준다.

TodoApp.test.js

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

describe('<TodoApp />', () => {
  it('renders TodoForm and TodoItemList', () => {
    const { getByText, getByTestId } = render(<TodoApp />);
    getByText('등록'); // TodoForm 존재유무 확인
    getByTestId('TodoItemList'); // TodoItemList 존재유무 확인
  });
});

그런 다음 테스트케이스를 통과시키는 코드를 작성해보자.
이 때, TodoItemList를 렌더링 할 때에는
아직 todos에 관한 테스트케이스가 없기 때문에,
임시적으로 todos props에 빈 배열을 설정한다.

TodoApp.js

import React from 'react';
import TodoItemList from './TodoItemList';
import TodoForm from './TodoForm';

const TodoApp = () => {
  return (
    <>
      <TodoForm />
      <TodoItemList todos={[]} />
    </>
  );
};

export default TodoApp;

todos 항목이 보이는지 확인하기

컴포넌트들의 렌더링이 확인되었다면,
다음작업으로는 임시로 넣어주었던 todos 배열에서의 할일 항목이
제대로 보여지는지 확인해준다.

TodoApp.test.js

...

it('renders two defaults todos', () => {
    const { getByText } = render(<TodoApp />);
    getByText('TDD 배우기');
    getByText('react-testing-library 사용하기');
  });

테스트케이스 통과를 위해서
TodoApp에서 setState함수를 통해 todos 배열에 관한 state관리를 해준다.

TodoApp.js

import React, { useState } from 'react';
import TodoItemList from './TodoItemList';
import TodoForm from './TodoForm';

const TodoApp = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: 'TDD 배우기',
      done: true
    },
    {
      id: 2,
      text: 'react-testing-library 사용하기',
      done: true
    }
  ]);
  return (
    <>
      <TodoForm />
      <TodoItemList todos={todos} />
    </>
  );
};

export default TodoApp;

새 항목 추가기능 구현하기

할일 항목이 잘 보여지게 되었다면,
새로운 항목을 추가했을 경우에 대해 테스트케이스를 작성한다.

TodoApp.test.js

...

it('creates new todo', () => {
    const { getByPlaceholderText, getByText } = render(<TodoApp />);
    // 이벤트를 발생시켜서 새 항목을 추가하면
    fireEvent.change(getByPlaceholderText('할 일을 입력하세요'), {
      target: {
        value: '새 항목 추가하기'
      }
    });
    fireEvent.click(getByText('등록'));
    // 해당 항목이 보여져야합니다.
    getByText('새 항목 추가하기');
  });

이 테스트케이스에서는 이벤트 발생을 시킨 후 새로 추가된 항목이 있는지까지 확인한다.
그런 후 테스트케이스 통과시키기.

TodoApp.js

import React, { useState, useCallback, useRef } from 'react';
import TodoItemList from './TodoItemList';
import TodoForm from './TodoForm';

const TodoApp = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: 'TDD 배우기',
      done: true
    },
    {
      id: 2,
      text: 'react-testing-library 배우기',
      done: true
    }
  ]);
  const nextId = useRef(3); // 새로 추가 할 항목에서 사용 할 id

  const onInsert = useCallback(
    text => {
      // 새 항목 추가 후
      setTodos(
        todos.concat({
          id: nextId.current,
          text,
          done: false
        })
      );
      // nextId 값에 1 더하기
      nextId.current += 1;
    },
    [todos]
  );

  return (
    <>
      <TodoForm onInsert={onInsert} />
      <TodoItemList todos={todos} />
    </>
  );
};

export default TodoApp;

테스트가 통과되었다면 다음으로 넘어가자~

토글기능 구현하기

앞서서 TodoItem 컴포넌트에서 구현했던 것들에 대한
상태를 관리해주기 위해 테스트케이스를 작성해보자

TodoApp.test.js

...

it('toggles todo', () => {
    const { getByText } = render(<TodoApp />);
    const todoText = getByText('TDD 배우기');
  
    expect(todoText).toHaveStyle('text-decoration: line-through;');
    fireEvent.click(todoText);
    expect(todoText).not.toHaveStyle('text-decoration: line-through;');
    fireEvent.click(todoText);
    expect(todoText).toHaveStyle('text-decoration: line-through;');
  });

'TDD 배우기' 항목에 클릭이벤트를 발생시킨 후
텍스트에 text-decoration 스타일이 적용되는지 확인한다.
왜저렇게 중복되느냐 함은, 아마도
텍스트를 찾아서 클릭했을 때 선이 그어지는 스타일이 적용되는지,
또다시 클릭했을때 없어지는지에 대해 확인하느라 그런 듯 하다..

삭제기능 구현하기

마지막으로 삭제기능을 구현해보자!

앞에서 TodoItemList 에서는 첫번째 항목을 삭제하는 테스트를 했을 때
getAllByText를 사용해 '삭제'라는 텍스트를 찾았었다.
이번에는 할일 항목의 텍스트를 찾아 해당 엘리먼트의 sibling 엘리먼트를 참조하여
버튼을 선택해서 클릭 이벤트를 발생시켜보자.

TodoApp.test.js

...

 it('removes todo', () => {
    const { getByText } = render(<TodoApp />);
    const todoText = getByText('TDD 배우기');
    const removeButton = todoText.nextSibling;
    fireEvent.click(removeButton);
    expect(todoText).not.toBeInTheDocument(); // 페이지에서 사라졌음을 의미
  });

마지막으로 테스트케이스를 통과시켜보자.

TodoApp.js

import React, { useState, useCallback, useRef } from 'react';
import TodoItemList from './TodoItemList';
import TodoForm from './TodoForm';

const TodoApp = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: 'TDD 배우기',
      done: true
    },
    {
      id: 2,
      text: 'react-testing-library 배우기',
      done: true
    }
  ]);
  const nextId = useRef(3); 

  const onInsert = useCallback(
    text => {
      setTodos(
        todos.concat({
          id: nextId.current,
          text,
          done: false
        })
      );
      nextId.current += 1;
    },
    [todos]
  );

  const onToggle = useCallback(
    id => {
      setTodos(
        todos.map(todo =>
          todo.id === id ? { ...todo, done: !todo.done } : todo
        )
      );
    },
    [todos]
  );

  const onRemove = useCallback(
    id => {
      setTodos(todos.filter(todo => todo.id !== id));
    },
    [todos]
  );

  return (
    <>
      <TodoForm onInsert={onInsert} />
      <TodoItemList 
	todos={todos} 
	onToggle={onToggle} 
	onRemove={onRemove} 
      />
    </>
  );
};

export default TodoApp;

완성본!

이렇게 마지막 테스트케이스까지 통과에 성공하게 되면
터미널에 `$ yarn start로 브라우저에 띄워 확인하면 된다.
스타일링은 건너뛰고 기능 구현에만 집중한 todolist가 아래와 같이 띄워지게 된다.


처음엔 아직도 기능구현에 어려움을 느끼기에..
테스트케이스를 먼저 작성해서
기능을 테스트 한다는 것 자체가 어렵게 다가왔지만,
막상 계속 반복해서 작업을 해보고,
하나하나 뜯어서 확인하니까 전체적인 틀에 대한 이해가 높아진 것 같다.

특히 내가 어려워하는 state나 props는 신경쓰지 않는다는 점에서
react-testing-library에 대해 조금은 호감도가 높아졌다(?)

물론 아직도 어떤 자료를 보지 않고 작성할 수 있는 수준은 절대 아니지만..
알지 못했던 부분에 대한 내용을 하나씩 늘려가는 것에 대해
조금씩 흥미를 느끼게 되는 것 같다.
하지만 여전히 터미널에 fail이 뜨는건 괜히 무섭고 떨린다ㅎ
계속해서 연습하고 연습해야지!

profile
쉽게 사는건 재미가 없더군요, 새로 시작합니다🤓

0개의 댓글