#1: useState를 사용해 ToDo-List TDD 적용해보기

Song-Minhyung·2023년 5월 15일
0

목차

  1. useState를 사용해 ToDo-List TDD 적용해보기 👈 NOW
  2. Context API(TodoProvider) Test 해보기
  3. useState로 관리하던 상태를 ContextAPI로 변경 + 기능추가
  4. ToDo App에 삭제, 토글, 수정 기능 추가하기
  5. Context API를 Recoil로 변경하기 (recoil test)

❓TDD란 무엇인가❓

tdd는 test driven development의 약자이며 소프트웨어 개발 방법론 중의 하나다.
보통 프로그램을 개발할 때 개발 후 테스트를 하는데 tdd는 테스트 후 개발을 하는 방식이다.

  1. 테스트 케이스 작성
  2. 테스트케이스를 통과하는 코드 작성
  3. 작성한 코드 리팩토링

위의 3개 과정을 반복한다.
중요한건 성공하는 테스트 코드, 실패하는 테스트 코드를 모두 작성해야 한다.
그래야 불필요한 설계를 피할수있고 정확한 요구사항만 집중할 수 있기 때문이다!

👍 TDD로 코드를 작성했을 때 장점

객체 지향적인 코드 개발

위의 세 과정을 반복하면 모듈화가 자연스럽게 이루어진다.
그래서 의존성과 종속성이 낮은 모듈로 조합된 컴포넌트 개발이 가능하게 된다.

설계 수정시간의 단축

테스트 코드를 기반으로 컴포넌트를 작성하기 때문에
작성한 코드를 보지 않고도 테스트 코드에서 설계의 문제를 바로 찾아낼 수 있다.

유지보수(리팩토링)의 용이성

TDD는 기본적으로 Unit Test 기반으로 테스트코드를 작성해서
만약 나중에 문제가 생기면 각각 모듈별로 테스트를 진행하면
문제가 어디서 발생했는지 쉽게 찾을 수 있다.

테스트 문서의 대체 가능

보통 개발 후 테스트를 진행하면 보통 Integration Test에 지나지 않는다.
근데 TDD로 테스트를 자동화 시키면 정확한 테스트 근거를 산출할 수 있게된다!

👎 TDD로 코드를 작성했을 때 단점

긴 학습기간

TDD를 프로젝트에 도입하려면 필요한 지식을 습득하고 개발환경을 구축하는데 시간이 많이 든다.
저번에 TDD를 배우려고 했다가 1년정도 방치한 이유도 학습하기가 번거로웠기 때문이다...

생산성 저하

프로젝트를 진행할 때 코드의 어느 부분에서 예외상황이 발생할 지 모이기도 한다.
만약 개발기간이 짧다면 테스트 코드 작성 후 통과하기 위한 코드를 작성하면 비효율적일수도 있을것이다.

🙋 배우게 된 이유

문서화 작업을 시도하고 있는데 상당히 시간이 많이 드는 일이다. TDD도 시간이 많이들고 문서화 작업도 시간이 많이 들면 테스트 코드를 작성하는게 훨 낫다 생각했기에 다시 배우게 됐다.

그리고 코드를 리팩토링 하거나 설계가 바뀌어야 할때 초반에 잘못된 설계로 인해 에러가 발생할 때가 많았다. 그래서 이 부분에 들어가는 시간을 줄이기 위해 배우게됐다.

🏁 Todo List 설계

무언가 새로운 기술, 프레임워크 등을 배울 때 TodoList로 작성하는걸 좋아한다.
새로운 언어를 배울 때 Hello World 를 찍어보는것과 비슷한 이유라 생각된다.
그래서 이번에 TDD를 배울때도 TodoList를 작성해보려 한다.
이번 글에서는 todo 를 추가하는것 까지만 하고

그 전에 아래 명령어로 프로젝트 생성 후 작성을 시작했다.

▶ npx create-react-app tdd --template=typescript

만약 cra를 사용하지 않을경우 아래의 명령어로 설치하면 된다.

npm install --save-dev @testing-library/react
혹은
▶ yarn add --dev @testing-library/react

TodoList는 크게 아래와 같은 컴포넌트로 나눌 수 있다.

  1. <TodoItem> 컴포넌트: 1개의 할일을 보여주는 컴포넌트
  2. <TodoList> 컴포넌트: <TodoItem> 컴포넌트로 이루어진 할일 목록을 보여주는 컴포넌트
  3. <TodoForm> 컴포넌트: <input>, <button>으로 TodoList에 할일을 추가하는 컴포넌트
  4. <TodoApp>   컴포넌트: <TodoList>, <TodoForm>을 그려주고

1️⃣ TodoItem 작성

TodoItem Test

todo item은 props로 내려주는 item을 그려주기만 하면 될것같다.
테스트 할때 도 item이 span으로 제대로 그려지는지 test하면 된다.

// TodoItem.test.tsx
import { render, screen } from "@testing-library/react";
import TodoItem from "./TodoItem";

describe('<TodoItem/>', () => {
  it('has span & button', () => {
    const mockItem = "this is a item";
    render(<TodoItem item={mockItem}/>)
    expect(screen.getByText(mockItem)).toBeInTheDocument();
  })
})

render로 해당 컴포넌트를 그려준다.
그 후 화면에 mockItem 내용이 제대로 그려졌는지 확인하는 테스트 코드이다.
테스트 코드를 작성하고 터미널에서 npm test 를 하면 테스트 코드의 통과여부를 볼 수 있다.
아직 TodoItem을 통과하게 작성해주지 않았기에 FAIL 이라 나올것이다.

TodoItem

// TodoItem.tsx
const TodoItem = ({item}: {item: string}) => {
  return (
  <>
    <span>{item}</span>
  </>);
}

export default TodoItem;

이제 테스트 코드가 통과하게 코드를 작성해보자.
props 로 내려준 item을 span으로 그려주었다.
코드를 작성후 저장을 눌러주면 TodoItem.test.tsx 는 초록색으로 PASS라 뜨는걸 볼 수있다.

2️⃣ TodoList 작성

TodoList Test

todolist 컴포넌트는 todoList를 입력받으면 해당 내용들을 화면에 뿌려주면 된다.

//TodoList.test.tsx
import { render, screen } from "@testing-library/react"
import TodoList from "./TodoList"

describe('<TodoList/>', () => {
  const todoItems = [
    {id: 1, item: 'item1'},
    {id: 2, item: 'item2'}
  ];

  it('render with todos', () => {
    render(<TodoList todoItems={todoItems}/>);
    expect(screen.getByText(todoItems[0].item)).toBeInTheDocument();
    expect(screen.getByText(todoItems[1].item)).toBeInTheDocument();
  });
})

TodoItem과 마찬가지로 props를 넣어서 render() 해준다.
그 후 실제로 화면에 그려졌는지 expect.toBeInTheDocument()를 사용해 확인해준다.
이때도 역시 FAIL이 뜬다.

TodoList

import TodoItem from "./TodoItem";

const TodoList = ({todoItems}:{todoItems: {id: number, item: string}[]}) => {
    return (
    <ul data-testid="TodoList">
      {
        todoItems.map(({id, item}) =>(
          <TodoItem key={id} item={item}/>
        ))
      }
    </ul>);
  }
  
  export default TodoList;

위와같이 props로 내려온 todoItems를 map으로 그려주면 테스트코드는 통과하게 된다.

3️⃣ TodoForm 작성

위에서 작성한것과 같이 다음 순서는 TodoForm 이다.
TodoForm 안에는 inputbutton이 존재하게 된다.

그리고 props로 input 값이 변경되었을 때 바꿔주는 onChange 함수,
button이 클릭되었을 때 onSubmit 함수를 호출하고, input의 값은 비어지게 된다.

테스트 해봐야 할건 세개정도 인것같다.

  1. input 과 button이 제대로 렌더링 되는지 확인
  2. input이 변경되었을때 값이 적용되는지 확인
  3. button이 눌렸을 때 onSubmit(input값)이 호출되고, input값이 비어지는지.

아래 코드에서 이걸 테스트 해보려 한다.

TodoForm Test

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TodoForm from "./TodoForm"

describe('<TodoForm/>', () => {
  
  it('<input>, <button> 렌더링 테스트', () => {
    const onSubmit = jest.fn();
    render(<TodoForm onSubmit={onSubmit}/>);
    expect(screen.getByTestId('TodoFormInput')).toBeInTheDocument();
    expect(screen.getByTestId('TodoFormButton')).toBeInTheDocument();
  });

  it('input 변경', async () => {  
    const onSubmit = jest.fn();
    render(<TodoForm onSubmit={onSubmit}/>);
    const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
    const inputVal = 'test input';

    await userEvent.type(input, inputVal);
    expect(input.value).toBe(inputVal);
  });

  it('onInsert 함수가 불러지면 input값은 비워짐', async () => {
    const onSubmit = jest.fn();
    const inputVal = 'test input Val';
    render(<TodoForm onSubmit={onSubmit}/>);

    const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
    const button = screen.getByTestId('TodoFormButton') as HTMLButtonElement;
    
    await userEvent.type(input, inputVal);
    await userEvent.click(button);

    expect(onSubmit).toBeCalledWith(inputVal);
    expect(input.value).toBe("");
  })
});

it('input 변경`)

여기서는 유저가 inputVal을 입력했을 때 input의 값의 inputVal이 되는지 확인한다.

it('onInsert 함수가 불러지면 input값은 비워짐')

여기서는 유저가 입력후 버튼을 눌렀을 때 함수에 inputVal이 같이 넘어가는지,
그리고 input의 값이 비어지는지 확인한다.

TodoForm

import { useState} from 'react'

interface TtodoForm {
  onSubmit: (value: string) => void;
}

const TodoForm = ({onSubmit}: TtodoForm) => {
  const [value,setValue] = useState('')

  const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  }
  const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    onSubmit(value);
    setValue('');
    e.preventDefault();
  }
  
  return <>
    <form  onSubmit={handleOnSubmit}>
        <input
          data-testid="TodoFormInput"
          placeholder="할 일을 입력"
          onChange={handleOnChange}
          value={value}
          />
        <button data-testid="TodoFormButton">등록</button>
    </form>
  </>;
}
export default TodoForm;

4️⃣ TodoApp 작성

이제 드디어 마지막까지 왔다.
TodoApp만 작성하면 1차적으로 끝나게 된다.
TodoApp에서 테스트 할건 간단한다.

  1. input 에 값이 입력되고
  2. 버튼이 눌렸을 때
  3. 입력한 todo가 제대로 화면에 보이는지

를 확인하면 된다.

TodoApp Test

import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event";
import TodoApp from "./TodoApp"

describe('<TodoApp/>', () => {

  it('todoform, todolist render', () => {
    render(<TodoApp/>)
    screen.getByTestId('TodoFormButton')
    screen.getByTestId('TodoList');
  });

  it('new todo', async() => {
    render(<TodoApp/>)
    const input = screen.getByTestId("TodoFormInput");
    const button = screen.getByTestId("TodoFormButton");
    const todoList = screen.getByTestId("TodoList");

    await userEvent.type(input, "new todo");
    await userEvent.click(button);
    
    expect(todoList).toHaveTextContent("new todo");
  });
});

TodoApp

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

const TodoApp = () => {
  const [state, setState] = useState<{id: number, item: string}[]>([]);
  const nextId = useRef(1);

  const handleOnSubmit = (todo: string) => {
    setState(prev => ([...prev, {id: nextId.current++, item: todo}]));
  } 
  return (
    <div data-testid="TodoApp">
      <TodoForm onSubmit={handleOnSubmit}/>
      <TodoList data-testid="TodoList" todoItems={state}/>
    </div>
  );
};

export default TodoApp;

🏭 테스트 코드 리팩토링

여기 TodoForm 테스트 코드를 작성하는 부분을 보면 아래와 같같이 중복이 매우 많이 보인다.

const onSubmit = jest.fn();
render(<TodoForm onSubmit={onSubmit}/>);

해당 코드는 setup 이라는 함수를 만들어서 중복을 없애줄 수 있다.
그렇게 중복되는 부분을 setup 함수로 작성해주면

mport { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TodoForm from "./TodoForm"

describe('<TodoForm/>', () => {
  
  it('<input>, <button> 렌더링 테스트', () => {
    const onSubmit = jest.fn();
    render(<TodoForm onSubmit={onSubmit}/>);
    expect(screen.getByTestId('TodoFormInput')).toBeInTheDocument();
    expect(screen.getByTestId('TodoFormButton')).toBeInTheDocument();
  });

  it('input 변경', async () => {  
    const onSubmit = jest.fn();
    render(<TodoForm onSubmit={onSubmit}/>);
    const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
    const inputVal = 'test input';

    await userEvent.type(input, inputVal);
    expect(input.value).toBe(inputVal);
  });

  it('onInsert 함수가 불러지면 input값은 비워짐', async () => {
    const onSubmit = jest.fn();
    const inputVal = 'test input Val';
    render(<TodoForm onSubmit={onSubmit}/>);

    const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
    const button = screen.getByTestId('TodoFormButton') as HTMLButtonElement;
    
    await userEvent.type(input, inputVal);
    await userEvent.click(button);

    expect(onSubmit).toBeCalledWith(inputVal);
    expect(input.value).toBe("");
  })
});

위와같이 중복이 매우 많았던 코드에서

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TodoForm from "./TodoForm"

describe('<TodoForm/>', () => {
  
  const setup = () => {
    const onSubmit = jest.fn();
    render(<TodoForm onSubmit={onSubmit}/>);
    const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
    const button = screen.getByTestId('TodoFormButton') as HTMLButtonElement;
    return {
      input, button, onSubmit
    };
  }
  
  it('<input>, <button> 렌더링 테스트', () => {
    const {input, button} = setup();
    expect(input).toBeInTheDocument();
    expect(button).toBeInTheDocument();
  });

  it('input 변경', async () => {  
    const {input} = setup();
    const inputVal = 'test input';

    await userEvent.type(input, inputVal);
    expect(input.value).toBe(inputVal);
  });

  it('onInsert 함수가 불러지면 input값은 비워짐', async () => {
    const {input, button, onSubmit} = setup();
    const inputVal = 'test input Val';
    
    await userEvent.type(input, inputVal);
    await userEvent.click(button);

    expect(onSubmit).toBeCalledWith(inputVal)
    expect(input.value).toBe("");
  })
});

위와같이 중복도 없어지고, 보기 조금 더 편해진 코드로 바꿀수가 있다.

profile
기록하는 블로그

0개의 댓글