제목과 같이 TodoItem에 삭제, 토글, 수정 테스트 코드를 추가할 것이다.
이전 글에서 TodoContext를 테스트하고 정상적으로 작동하는걸 테스트코드로 확인했다.
이번에는 전에 만든걸 그냥 사용만 하면 된다.
그리고 기능을 한번에 많이 추가해서 테스트 코드가 한번에 길어질것같다...
TodoItem의 구현 세부사항은 아래와 같다.
item.value
값을 가진 span
과 수정 버튼
이 존재함.수정 버튼
클릭시 -> 완료 버튼
과 item.value
값을 가진 input
이 나옴input
에 값을 입력하고 완료버튼
을 누르면 changeTodo(id,value)
가 호출됨input
값이 비었는데 완료버튼
을 누르면 입력값이 없습니다
alert
가 표시됨.삭제버튼
클릭시 deleteTodo(id)
가 호출됨span
클릭시 가운데 줄이 생기고toggleTodo(id)
가 호출됨전에 테스트 코드를 리팩토링 했던것처럼 중복되는 부분을 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
};
}
만약 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;
// TodoItem.test.tsx
it('최초 렌더링시 item.value값을 가진 span과 수정버튼이 존재함', () => {
const {itemSpan, modifyButton} = setup();
expect(itemSpan.innerHTML).toBe(mockItem.value);
expect(modifyButton.innerHTML).toBe("수정");
});
수정버튼을 클릭하면 리렌더링 되며 "수정"이 "완료"로 바뀌므로 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);
});
// 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");
});
// 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("입력값이 없습니다.");
});
// TodoItem.test.tsx
it('삭제버튼 클릭시 deleteTodo(id)가 호출됨', async ()=>{
const {mockDeleteTodo, deleteButton} = setup();
await userEvent.click(deleteButton);
expect(mockDeleteTodo).toBeCalledWith(mockItem.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");
});
코드를 보면 바로 느껴지곘지만 수정 버튼 클릭 후 테스트하는 아래 세개의 테스트 코드에서
수정버튼 누르는 부분이 계속 중복으로 써주고 있다.
수정 버튼
클릭시 -> 완료 버튼
과 item.value
값을 가진 input
이 나옴input
에 값을 입력하고 완료버튼
을 누르면 changeTodo(id,value)
가 호출됨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를 적용하는게 좋지 않다는걸 이번에 알게됐고 상황에 맞춰 적용해야 할것같다.
끗.