TodoApp 만들기

우리가 마지막으로 만들 컴포넌트 TodoApp 컴포넌트를 만들어봅시다. 이 컴포넌트는 지금까지 만든 Todo 관련 컴포넌트들을 모두 사용하여 기능을 완성합니다. 이번 컴포넌트는 기존에 따로 따로 테스트가 이루어진 컴포넌트들을 함께 사용하여 구현을 하게 되므로, 이번에 작성하는 테스트 코드는 일종의 통합 테스트 입니다. 우선, 다음과 같이 비어있는 컴포넌트를 먼저 만드세요.

TodoApp.js

import React from 'react';

const TodoApp = () => {
  return (
    <div>

    </div>
  );
};

export default TodoApp;

그 다음엔 이 컴포넌트의 첫번째 테스트로 결과 화면에 하나의 form 태그와 두개의 li 태그가 존재하는지 검증을 하겠습니다.

TodoApp.test.js

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

describe('TodoApp', () => {
  it('renders correctly', () => {
    const { container } = render(<TodoApp />);
    const form = container.querySelector('form');
    expect(form).toBeTruthy(); // form 태그가 있는지 호가인
    const items = container.querySelectorAll('li');
    expect(items.length).toBe(2); // 두개가 li 태그가 있는지 확인
  });
});

이 테스트 케이스를 통과시켜봅시다.

TodoApp.js

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

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

export default TodoApp;

이번에는 새 항목을 추가하는 기능을 위한 테스트를 작성해봅시다.

TodoApp.test.js

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

describe('TodoApp', () => {
  it('renders correctly', () => {
    const { container } = render(<TodoApp />);
    const form = container.querySelector('form');
    expect(form).toBeTruthy(); // form 태그가 있는지 호가인
    const items = container.querySelectorAll('li');
    expect(items.length).toBe(2); // 두개가 li 태그가 있는지 확인
  });

  it('inserts new todo', () => {
    const { container, getByText } = render(<TodoApp />);

    // 인풋 수정
    const input = container.querySelector('input');
    fireEvent.change(input, {
      target: { value: '일정 관리 애플리케이션 만들기' }
    });

    // 등록 버튼 클릭
    const button = getByText('등록');
    fireEvent.click(button);

    // 3개의 투두가 있고, 방금 추가한 텍스트가 존재하는지 확인
    const items = container.querySelectorAll('li');
    expect(items.length).toBe(3);
    getByText('일정 관리 애플리케이션 만들기');
  });
});

그 다음엔 이 테스트 케이스를 통과시키기 위한 구현을 해보세요.

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

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

  // 새로운 항목을 만들 때마다 1씩 올라가는 고유 id 값
  // 렌더링 될 필요가 없으니 ref 로 동적 값 관리
  const id = useRef(3);

  const onInsert = useCallback(
    text => {
      setTodos(
        todos.concat({
          id: id.current++,
          text,
          done: false
        })
      );
    },
    // 현재 상태에 변화를 반영하는 것이므로 useCallback 을 쓴다면 이부분에 todos 가 바뀔 때 마다
    // 콜백 함수를 새로 만들도록 배열 안에 todos 넣기
    [todos]
  );

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

export default TodoApp;

이제 토글 기능을 위한 테스트 코드를 작성해봅시다.

TodoApp.test.js

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

describe('TodoApp', () => {
  it('renders correctly', () => {
    const { container } = render(<TodoApp />);
    const form = container.querySelector('form');
    expect(form).toBeTruthy(); // form 태그가 있는지 호가인
    const items = container.querySelectorAll('li');
    expect(items.length).toBe(2); // 두개가 li 태그가 있는지 확인
  });

  it('inserts new todo', () => {
    const { container, getByText } = render(<TodoApp />);

    // 인풋 수정
    const input = container.querySelector('input');
    fireEvent.change(input, {
      target: { value: '일정 관리 애플리케이션 만들기' }
    });

    // 등록 버튼 클릭
    const button = getByText('등록');
    fireEvent.click(button);

    // 3개의 투두가 있고, 방금 추가한 텍스트가 존재하는지 확인
    const items = container.querySelectorAll('li');
    expect(items.length).toBe(3);
    getByText('일정 관리 애플리케이션 만들기');
  });

  it('toggles first todo', () => {
    // 첫번째 할 일 토글하기
    const { getByText } = render(<TodoApp />);
    const span = getByText('TDD 배우기');
    expect(span.style.textDecoration).toBe('line-through'); // 취소선이 그어져있는지 확인
    fireEvent.click(span); // 클릭
    expect(span.style.textDecoration).toBe('none'); // 취소선이 사라졌는지 확인
  });
});

토글 기능을 구현해서 이 테스트 코드를 통과시킵시다.

TodoApp.js

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

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

  // 새로운 항목을 만들 때마다 1씩 올라가는 고유 id 값
  // 렌더링 될 필요가 없으니 ref 로 동적 값 관리
  const id = useRef(3);

  const onInsert = useCallback(
    text => {
      setTodos(
        todos.concat({
          id: id.current++,
          text,
          done: false
        })
      );
    },
    // 현재 상태에 변화를 반영하는 것이므로 useCallback 을 쓴다면 이부분에 todos 가 바뀔 때 마다
    // 콜백 함수를 새로 만들도록 배열 안에 todos 넣기
    [todos]
  );

  const onToggle = useCallback(
    id => {
      setTodos(
        todos.map(todo =>
          todo.id === id ? { ...todo, done: !todo.done } : todo
        )
      );
    },
    [todos] // 여기도 마찬가지로 todos 가 바뀔 때 마다 콜백 함수를 새로 만들도록 명시해줌
  );

  return (
    <div>
      <TodoForm onInsert={onInsert} />
      <TodoList todos={todos} onToggle={onToggle} />
    </div>
  );
};

export default TodoApp;

마지막으로, 할 일 항목을 제거하는 테스트 케이스를 작성해봅시다.

TodoApp.test.js

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

describe('TodoApp', () => {
  it('renders correctly', () => {
    const { container } = render(<TodoApp />);
    const form = container.querySelector('form');
    expect(form).toBeTruthy(); // form 태그가 있는지 호가인
    const items = container.querySelectorAll('li');
    expect(items.length).toBe(2); // 두개가 li 태그가 있는지 확인
  });

  it('inserts new todo', () => {
    const { container, getByText } = render(<TodoApp />);

    // 인풋 수정
    const input = container.querySelector('input');
    fireEvent.change(input, {
      target: { value: '일정 관리 애플리케이션 만들기' }
    });

    // 등록 버튼 클릭
    const button = getByText('등록');
    fireEvent.click(button);

    // 3개의 투두가 있고, 방금 추가한 텍스트가 존재하는지 확인
    const items = container.querySelectorAll('li');
    expect(items.length).toBe(3);
    getByText('일정 관리 애플리케이션 만들기');
  });

  it('toggles first todo', () => {
    // 첫번째 할 일 토글하기
    const { getByText } = render(<TodoApp />);
    const span = getByText('TDD 배우기');
    expect(span.style.textDecoration).toBe('line-through'); // 취소선이 그어져있는지 확인
    fireEvent.click(span); // 클릭
    expect(span.style.textDecoration).toBe('none'); // 취소선이 사라졌는지 확인
  });

  it('removes first todo', () => {
    // 첫번째 할 일 제거하기
    const { getByText, queryByText, container } = render(<TodoApp />);
    const span = getByText('TDD 배우기');
    const button = span.nextElementSibling; // 바로 옆에있는 버튼 선택하기
    fireEvent.click(button); // 삭제 버튼 크릵

    expect(queryByText('TDD 배우기')).toBeFalsy(); // TDD 배우기가 더 이상 존재하지 않음
    expect(container.querySelectorAll('li').length).toBe(1); // 한개의 항목만 보여짐
  });
});

여기서는 queryByText 라는 함수를 사용했는데요, 이 함수는 getByText 와 달리 원하는 목표물이 존재하지 않을 때 테스트가 실패하지 않고 그냥 null 이라는 결과를 반환합니다. 따라서, 특정 내용이 없는 지 검사해야 할 때 사용하면 편합니다.

테스트 케이스를 다 작성하셨으면 삭제 기능을 구현해봅시다.

TodoApp.js

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

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

  // 새로운 항목을 만들 때마다 1씩 올라가는 고유 id 값
  // 렌더링 될 필요가 없으니 ref 로 동적 값 관리
  const id = useRef(3);

  const onInsert = useCallback(
    text => {
      setTodos(
        todos.concat({
          id: id.current++,
          text,
          done: false
        })
      );
    },
    // 현재 상태에 변화를 반영하는 것이므로 useCallback 을 쓴다면 이부분에 todos 가 바뀔 때 마다
    // 콜백 함수를 새로 만들도록 배열 안에 todos 넣기
    [todos]
  );

  const onToggle = useCallback(
    id => {
      setTodos(
        todos.map(todo =>
          todo.id === id ? { ...todo, done: !todo.done } : todo
        )
      );
    },
    [todos] // 여기도 마찬가지로 todos 가 바뀔 때 마다 콜백 함수를 새로 만들도록 명시해줌
  );

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

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

export default TodoApp;

테스트 케이스가 모두 통과했나요? 그러면 App 에서 TodoApp 을 렌더링해보세요.

App.js

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

const App = () => {
  return (
    <div>
      <TodoApp />
    </div>
  );
};

export default App;

기존에 우리가 작성했던 테스트 케이스들이 모두 통과했다면, 브라우저 상에 나타나는 실제 결과물도 잘 작동할 것입니다. 모든 기능이 잘 작동하는지 확인해보세요.

results matching ""

    No results matching ""