TodoItem, TodoList, TodoApp, TodoForm 을 테스트 코드 작성 -> 코드 작성 순으로 진행했다.
이번에는 저번에 구현했던 내용을 Context api로 바꿀것이다.
왜냐하면 지금 TodoItem을 핸들링 해주는 함수가 TodoApp -> TodoList -> TodoItem으로 전달되고
TodoList는 쓰지도 않는 props를 전달하기 위해 받기만 하고 있기 때문이다.
지금은 그저 한개라고 생각 할수도 있지만 컴포넌트들이 많아진다면 이 깊이는 더 깊어질 것이다.
그래서 일단 context를 구현하고, provider를 테스트 해보려 한다.
TodoContext는 TodoList를 저장한다.
그리고 addTodo, deleteTodo 함수를 구현해야 한다.
아래는 위 내용을 구현한 코드이다.
//TodoContext.tsx
import { createContext, ReactNode, useRef, useState } from "react";
export interface Todo {
id: number;
value: string;
}
export interface TodoContextState {
Todos: Todo[],
addTodo: (value: string) => void;
deleteTodo: (id: number) => void;
}
const TodoContext = createContext<TodoContextState | null>(null);
const TodoProvider = ({children, value, init = []}: {children: ReactNode, value?: TodoContextState, init?: Todo[]}) => {
const [Todos, setState] = useState<Todo[]>(init ?? value?.Todos);
const nextId = useRef(0);
const addTodo = (value: string) => {
setState(prev => [...prev, {id: nextId.current++, value}]);
}
const deleteTodo = (id: number) => {
setState(prev => prev.filter(todo => todo.id !== id));
}
return (
<TodoContext.Provider value={value ?? {Todos, addTodo, deleteTodo}}>
{children}
</TodoContext.Provider>
);
}
export {TodoContext, TodoProvider};
위에서 TodoProvider가 init과 value를 가져오는 이유는 테스트 할 때 목업을 넣어주기 위함이다.
이제 위의 provider를 테스트 해 줄 차례인데 그러기 위해선 test component를 만들어야 한다.
const TestComponent = () => {
const {Todos, addTodo, deleteTodo} = useTodoContext();
return (
<>
<button
data-testid="addTodo"
onClick={() => addTodo(newTodoValue)}
> add Todo Button </button>
{Todos.map(todo => (
<div key={todo.id}>
{todo.value}
<div
data-testid="deleteTodo"
onClick={() => deleteTodo(todo.id)}
> delete Todo Button</div>
</div>
))}
</>
);
}
TestComponent는 실제로 TodoProvider를 사용해서 값이 제대로 들어가는지, 제거되는지만 테스트 하면 된다.
그러므로 최소한의 모양으로 TestComponent를 작성한다.
참고로 useTodoContext는 provider 안에 있다면 context를 리턴해주고
만약 provider 밖에 있다면 에러를 발생시켜주는 훅이다.
//useTodoContext.tsx
import { useContext } from "react"
import { TodoContext } from "../context/TodoContext"
const useTodoContext = () => {
const state = useContext(TodoContext);
if (!state) {
throw new Error('cannot find TodoContext Provider');
}
return state;
}
export default useTodoContext;
이제 TestComponent를 만들었으니
이 컴포넌트를 render 해서 테스트를 해주기만 하면 된다.
원래 TDD는 테스트 코드 작성 -> 코드 작성을 해야하는데
아직 어떻게 해야할지 감이 오질 않아서 반대로 작성했다.
그런데 이렇게 작성하면서 앞으로 context api 와 같은
상태관리 라이브러리의 테스트 코드 작성을 어떤식으로 해야할지 감이 왔다!
//TodoContext.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { TodoProvider } from "./TodoContext";
import { mockTodos } from "../hooks/useMockTodoContext";
import useTodoContext from "../hooks/useTodoContext";
const newTodoValue = "add new todo";
describe("TodoProvider Test", () => {
const TestComponent = () => {
const {Todos, addTodo, deleteTodo} = useTodoContext();
return (
<>
<button
data-testid="addTodo"
onClick={() => addTodo(newTodoValue)}
> add Todo Button </button>
{Todos.map(todo => (
<div key={todo.id}>
{todo.value}
<div
data-testid="deleteTodo"
onClick={() => deleteTodo(todo.id)}
> delete Todo Button</div>
</div>
))}
</>
);
}
const setup = () => {
render(<TodoProvider init={mockTodos}><TestComponent/></TodoProvider>);
}
it("addTodo 확인, newTodoValue 추가 되는지", async () => {
setup();
const addButton = screen.getByTestId("addTodo")
await userEvent.click(addButton);
expect(screen.getByText(newTodoValue)).toBeInTheDocument();
});
it("deleteTodo 확인, 리스트중 첫번째 todo 지워지는지", async () => {
setup();
const deleteButton = screen.getAllByTestId("deleteTodo")[0];
await userEvent.click(deleteButton);
expect(screen.queryByText(mockTodos[0].value)).not.toBeInTheDocument();
});
});
이렇게 테스트를 통해 context api의 함수들은 제대로된 작동이 보장되었으므로
앞으로 다른 컴포넌트에서 해당 함수들을 사용할 때는 함수가 제대로 호출되는지,
값은 제대로 들어가는지만 테스트 해주면 될것같다!