#4: TodoItem 에 삭제, 토글, 수정 기능 추가하기

Song-Minhyung·2023년 5월 25일
0
post-thumbnail

목차

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

📜 이번에 할것

제목과 같이 TodoItem에 삭제, 토글, 수정 테스트 코드를 추가할 것이다.
이전 글에서 TodoContext를 테스트하고 정상적으로 작동하는걸 테스트코드로 확인했다.
이번에는 전에 만든걸 그냥 사용만 하면 된다.
그리고 기능을 한번에 많이 추가해서 테스트 코드가 한번에 길어질것같다...

TodoItem 기능 추가

TodoItem의 구현 세부사항은 아래와 같다.

  1. 최초 렌더링시 -> item.value값을 가진 span수정 버튼이 존재함.
  2. 수정 버튼 클릭시 -> 완료 버튼item.value값을 가진 input이 나옴
  3. input에 값을 입력하고 완료버튼을 누르면 changeTodo(id,value)가 호출됨
  4. input값이 비었는데 완료버튼을 누르면 입력값이 없습니다 alert가 표시됨.
  5. 삭제버튼 클릭시 deleteTodo(id)가 호출됨
  6. span 클릭시 가운데 줄이 생기고toggleTodo(id)가 호출됨

setup함수

전에 테스트 코드를 리팩토링 했던것처럼 중복되는 부분을 setup 함수에 전부 넣어줬다.

// TodoItem.test.tsx
const setup = () => {
  const {
    mockAddTodo, 
    mockDeleteTodo, 
    mockToggleTodo, 
    mockChangeTodo, 
    mockChangeModifyMode, 
    options
  } = renderWithContext(<TodoItem item={mockItem}/>);

  const deleteButton = screen.getByTestId("DeleteButton") as HTMLButtonElement
  const modifyButton = screen.getByTestId("ModifyButton") as HTMLButtonElement;
  const modifyInput = screen.queryByTestId("ModifyInput") as HTMLInputElement;
  const itemSpan = screen.queryByText(mockItem.value) as HTMLSpanElement;

  return { 
    mockAddTodo,
    mockDeleteTodo, 
    mockToggleTodo, 
    mockChangeTodo, 
    mockChangeModifyMode, 
    options,
    itemSpan, 
    deleteButton, 
    modifyButton, 
    modifyInput
  };
}

renderWithContext 함수

만약 Provider를 사용하며 테스트 하려면 컴포넌트를 Provider로 감싸줘야 한다.
근데 renderWithContext함수를 생성하지 않았다면 함수 안의 내용이 다른 테스트에서도 반복됐을 것이다.

// renderWithContext.tsx
export const mockTodos:Todo[] = [
  {id: 1, value: 'Test todo1', done: false, modifyMode: false},
  {id: 2, value: 'Test todo2', done: false, modifyMode: false},
];

const renderWithContext = (
  renderTargetComponent: 
    React.ReactElement<
      | any, string
      | React.JSXElementConstructor<any>
    >
) => {
  const mockAddTodo = jest.fn();
  const mockDeleteTodo = jest.fn();
  const mockToggleTodo = jest.fn();
  const mockChangeTodo = jest.fn();
  const mockChangeModifyMode = jest.fn();
  

  const mockTodoContext: TodoContextState = {
    Todos: mockTodos,
    addTodo: mockAddTodo,
    deleteTodo: mockDeleteTodo,
    changeTodo: mockChangeTodo,
    toggleTodo: mockToggleTodo,
    changeModifyMode: mockChangeModifyMode,
  };


  const wrapper = ({children}: {children: React.ReactNode}) => (
    <TodoContext.Provider value={mockTodoContext}>{children}</TodoContext.Provider>
  );

  const options = render(renderTargetComponent, {wrapper});

  return {
    mockDeleteTodo, 
    mockAddTodo, 
    mockToggleTodo, 
    mockChangeTodo, 
    mockChangeModifyMode, 
    wrapper,
    options
  };
}

export default renderWithContext;

describe('<TodoItem/>')

1️⃣ it('최초 렌더링시 item.value값을 가진 span과 수정버튼이 존재함')

// TodoItem.test.tsx
it('최초 렌더링시 item.value값을 가진 span과 수정버튼이 존재함', () => {
  const {itemSpan, modifyButton} = setup();

  expect(itemSpan.innerHTML).toBe(mockItem.value);
  expect(modifyButton.innerHTML).toBe("수정");
});

2️⃣ it('수정버튼 클릭시 item.value값을 가진 input과 완료버튼이 존재함')

수정버튼을 클릭하면 리렌더링 되며 "수정"이 "완료"로 바뀌므로 rerender 함수를 사용해줬다.
나머지 테스트에서 rerender 함수를 사용하는 이유도 동일하다.

// TodoItem.test.tsx
it('수정버튼 클릭시 item.value값을 가진 input과 완료버튼이 존재함', async () => {
  const {mockChangeModifyMode, modifyButton, options} = setup();

  await userEvent.click(modifyButton);
  expect(mockChangeModifyMode).toBeCalledWith(mockItem.id);

  options.rerender(<TodoItem item={{...mockItem, modifyMode: true}}/>);

  const modifyInput = screen.queryByTestId("ModifyInput") as HTMLInputElement;
  expect(modifyButton.innerHTML).toBe("완료");
  expect(modifyInput.value).toBe(mockItem.value);
});

3️⃣ it('완료버튼 클릭시 값이 있다면 changeTodo 함수를 호출함 ')

// TodoItem.test.tsx
it('완료버튼 클릭시 값이 있다면 changeTodo 함수를 호출함', async () => {
  const {mockChangeModifyMode, mockChangeTodo, modifyButton , options} = setup();

  await userEvent.click(modifyButton);
  expect(mockChangeModifyMode).toBeCalledWith(mockItem.id);

  options.rerender(<TodoItem item={{...mockItem, modifyMode: true}}/>);

  const modifyInput = screen.queryByTestId("ModifyInput") as HTMLInputElement;

  await userEvent.clear(modifyInput);
  await userEvent.type(modifyInput, "new Todo");
  await userEvent.click(modifyButton);

  expect(mockChangeTodo).toBeCalledWith(mockItem.id, "new Todo");
});

4️⃣ it('완료버튼 클릭시 값이 없다면 alert(입력값이 없습니다.) 를 출력함')

// TodoItem.test.tsx
it('완료버튼 클릭시 값이 없다면 alert(입력값이 없습니다.) 를 출력함', async () => {
  const {mockChangeModifyMode, modifyButton , options} = setup();

  await userEvent.click(modifyButton);
  expect(mockChangeModifyMode).toBeCalledWith(mockItem.id);

  options.rerender(<TodoItem item={{...mockItem, modifyMode: true}}/>);

  const modifyInput = screen.queryByTestId("ModifyInput") as HTMLInputElement;
  window.alert = jest.fn();

  await userEvent.clear(modifyInput);
  await userEvent.click(modifyButton);

  expect(window.alert).toBeCalledWith("입력값이 없습니다.");
});

5️⃣ it('삭제버튼 클릭시 deleteTodo(id)가 호출됨')

// TodoItem.test.tsx
it('삭제버튼 클릭시 deleteTodo(id)가 호출됨', async ()=>{
  const {mockDeleteTodo, deleteButton} = setup();

  await userEvent.click(deleteButton);

  expect(mockDeleteTodo).toBeCalledWith(mockItem.id);
});

6️⃣ it('span 클릭시 "line-through" class 생기고toggleTodo(id)가 호출됨, 다시한번 클릭시 없어짐')

// TodoItem.test.tsx
it('span 클릭시 가운데 줄이 생기고toggleTodo(id)가 호출됨, 다시한번 클릭시 줄이 없어짐', async ()=>{
  const {mockToggleTodo, itemSpan, options} = setup();

  await userEvent.click(itemSpan);
  options.rerender(<TodoItem item={{...mockItem, done: true}}/>);

  expect(mockToggleTodo).toBeCalledWith(mockItem.id);
  expect(itemSpan).toHaveClass("line-through");


  await userEvent.click(itemSpan);
  options.rerender(<TodoItem item={{...mockItem, done: false}}/>);

  expect(mockToggleTodo).toBeCalledWith(mockItem.id);
  expect(itemSpan).not.toHaveClass("line-through");
});

test code 리팩토링

코드를 보면 바로 느껴지곘지만 수정 버튼 클릭 후 테스트하는 아래 세개의 테스트 코드에서
수정버튼 누르는 부분이 계속 중복으로 써주고 있다.

  1. 수정 버튼 클릭시 -> 완료 버튼item.value값을 가진 input이 나옴
  2. input에 값을 입력하고 완료버튼을 누르면 changeTodo(id,value)가 호출됨
  3. input값이 비었는데 완료버튼을 누르면 입력값이 없습니다 alert가 표시됨.
const {...} = setup();

await userEvent.click(modifyButton);
expect(mockChangeModifyMode).toBeCalledWith(mockItem.id);

options.rerender(<TodoItem item={{...mockItem, modifyMode: true}}/>);

const modifyInput = screen.queryByTestId("ModifyInput") as HTMLInputElement;

바로 이부분!!
이부분을 아래처럼 clickModifyButton 함수로 따로 떼어준다.

const clickModifyButton = async() => {
  const setups = setup();
  await userEvent.click(setups.modifyButton);
  expect(setups.mockChangeModifyMode).toBeCalledWith(mockItem.id);

  setups.options.rerender(<TodoItem item={{...mockItem, modifyMode: true}}/>);

  const modifyInput = screen.queryByTestId("ModifyInput") as HTMLInputElement;

  return {...setups, modifyInput};
}

그리고서 중복됐던 부분에는 해당 함수를 넣어주면 읽기 좋게 깔끔하게 변경된다.

it('수정버튼 클릭시 item.value값을 가진 input과 완료버튼이 존재함', async () => {
  const {modifyButton, modifyInput} = await clickModifyButton();

  expect(modifyButton.innerHTML).toBe("완료");
  expect(modifyInput.value).toBe(mockItem.value);
});

it('완료버튼 클릭시 값이 있다면 changeTodo 함수를 호출함', async () => {
  const {mockChangeTodo, modifyButton, modifyInput} = await clickModifyButton();

  await userEvent.clear(modifyInput);
  await userEvent.type(modifyInput, "new Todo");
  await userEvent.click(modifyButton);

  expect(mockChangeTodo).toBeCalledWith(mockItem.id, "new Todo");
});

it('완료버튼 클릭시 값이 없다면 alert(입력값이 없습니다.) 를 출력함', async () => {
  const {modifyInput, modifyButton} = await clickModifyButton();
  window.alert = jest.fn();

  await userEvent.clear(modifyInput);
  await userEvent.click(modifyButton);

  expect(window.alert).toBeCalledWith("입력값이 없습니다.");
});

끗 🔚

이제 실제로 npm run start 명령여로 실행을 해보면
위의 테스트 사항이 모두 제대로 반영된걸 확인할 수 있따.

얼마전에 코테를 봤는데 알고리즘 + 과제 문제가 주어졌다.
알고리즘을 풀고 과제 문제에서 테스트 코드를 작성하며 컴포넌트들을 만들었더니
모든 구현사항을 모두 구현하지 못했다...
아마 테스트 코드를 작성하지 않았다면 주어진 시간내에 구현을 할 수 있었을것같다.

TDD의 단점이 여기서 이렇게 드러나는데 초반에 테스트 코드를 작성하는데 시간이 너무 많이든다.

그래서 무조건 TDD를 적용하는게 좋지 않다는걸 이번에 알게됐고 상황에 맞춰 적용해야 할것같다.
끗.

profile
기록하는 블로그

0개의 댓글