Todolist
의 마지막 작업인 TodoApp
작업은
앞에서 따로 진행했던 유닛테스트를 거친 컴포넌트를 사용해서 구현하기 때문에
일종의 최종점검을 하는 통합테스트 작업이라고 볼 수 있다.
이 컴포넌트에서는 todos
배열에 대한 모든 상태관리를 담당한다.
우선 TodoApp
컴포넌트의 틀을 잡아준 다음,
그 안에 품어줄 컴포넌트들을 제대로 불러오는지 확인을 해주기 위해
TodoItemList
에 data-testid를 설정해준다.
🤔
근거 없는 나의 뇌피셜입니다
TodoItemList
에만 testid를 설정해주는 이유는 ❓
TodoApp
의 테스트케이스에서 컴포넌트들의 존재유무를 확인해야 하는데,TodoItemList
에는 별다른 항목이 없기 때문에
getByTestId
로 찾기 위해 따로 설정을 해주는 듯 하다.
➡︎querySelector
로 찾을수도 있지만, 공식문서에서는 DOM API로 선택하기보단 data-testid를 사용하기를 권장한다.
TodoItemList.js
<ul data-testid="TodoItemList">
...
</ul>
이렇게 testid를 설정해주었다면 TodoApp
의 테스트케이스를 작성해준다.
TodoApp.test.js
import React from 'react';
import TodoApp from './TodoApp';
import { render } from 'react-testing-library';
describe('<TodoApp />', () => {
it('renders TodoForm and TodoItemList', () => {
const { getByText, getByTestId } = render(<TodoApp />);
getByText('등록'); // TodoForm 존재유무 확인
getByTestId('TodoItemList'); // TodoItemList 존재유무 확인
});
});
그런 다음 테스트케이스를 통과시키는 코드를 작성해보자.
이 때, TodoItemList
를 렌더링 할 때에는
아직 todos
에 관한 테스트케이스가 없기 때문에,
임시적으로 todos props
에 빈 배열을 설정한다.
TodoApp.js
import React from 'react';
import TodoItemList from './TodoItemList';
import TodoForm from './TodoForm';
const TodoApp = () => {
return (
<>
<TodoForm />
<TodoItemList todos={[]} />
</>
);
};
export default TodoApp;
컴포넌트들의 렌더링이 확인되었다면,
다음작업으로는 임시로 넣어주었던 todos
배열에서의 할일 항목이
제대로 보여지는지 확인해준다.
TodoApp.test.js
...
it('renders two defaults todos', () => {
const { getByText } = render(<TodoApp />);
getByText('TDD 배우기');
getByText('react-testing-library 사용하기');
});
테스트케이스 통과를 위해서
TodoApp
에서 setState
함수를 통해 todos
배열에 관한 state관리를 해준다.
TodoApp.js
import React, { useState } from 'react';
import TodoItemList from './TodoItemList';
import TodoForm from './TodoForm';
const TodoApp = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: 'TDD 배우기',
done: true
},
{
id: 2,
text: 'react-testing-library 사용하기',
done: true
}
]);
return (
<>
<TodoForm />
<TodoItemList todos={todos} />
</>
);
};
export default TodoApp;
할일 항목이 잘 보여지게 되었다면,
새로운 항목을 추가했을 경우에 대해 테스트케이스를 작성한다.
TodoApp.test.js
...
it('creates new todo', () => {
const { getByPlaceholderText, getByText } = render(<TodoApp />);
// 이벤트를 발생시켜서 새 항목을 추가하면
fireEvent.change(getByPlaceholderText('할 일을 입력하세요'), {
target: {
value: '새 항목 추가하기'
}
});
fireEvent.click(getByText('등록'));
// 해당 항목이 보여져야합니다.
getByText('새 항목 추가하기');
});
이 테스트케이스에서는 이벤트 발생을 시킨 후 새로 추가된 항목이 있는지까지 확인한다.
그런 후 테스트케이스 통과시키기.
TodoApp.js
import React, { useState, useCallback, useRef } from 'react';
import TodoItemList from './TodoItemList';
import TodoForm from './TodoForm';
const TodoApp = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: 'TDD 배우기',
done: true
},
{
id: 2,
text: 'react-testing-library 배우기',
done: true
}
]);
const nextId = useRef(3); // 새로 추가 할 항목에서 사용 할 id
const onInsert = useCallback(
text => {
// 새 항목 추가 후
setTodos(
todos.concat({
id: nextId.current,
text,
done: false
})
);
// nextId 값에 1 더하기
nextId.current += 1;
},
[todos]
);
return (
<>
<TodoForm onInsert={onInsert} />
<TodoItemList todos={todos} />
</>
);
};
export default TodoApp;
테스트가 통과되었다면 다음으로 넘어가자~
앞서서 TodoItem
컴포넌트에서 구현했던 것들에 대한
상태를 관리해주기 위해 테스트케이스를 작성해보자
TodoApp.test.js
...
it('toggles todo', () => {
const { getByText } = render(<TodoApp />);
const todoText = getByText('TDD 배우기');
expect(todoText).toHaveStyle('text-decoration: line-through;');
fireEvent.click(todoText);
expect(todoText).not.toHaveStyle('text-decoration: line-through;');
fireEvent.click(todoText);
expect(todoText).toHaveStyle('text-decoration: line-through;');
});
'TDD 배우기' 항목에 클릭이벤트를 발생시킨 후
텍스트에 text-decoration 스타일이 적용되는지 확인한다.
왜저렇게 중복되느냐 함은, 아마도
텍스트를 찾아서 클릭했을 때 선이 그어지는 스타일이 적용되는지,
또다시 클릭했을때 없어지는지에 대해 확인하느라 그런 듯 하다..
마지막으로 삭제기능을 구현해보자!
앞에서 TodoItemList
에서는 첫번째 항목을 삭제하는 테스트를 했을 때
getAllByText
를 사용해 '삭제'라는 텍스트를 찾았었다.
이번에는 할일 항목의 텍스트를 찾아 해당 엘리먼트의 sibling
엘리먼트를 참조하여
버튼을 선택해서 클릭 이벤트를 발생시켜보자.
TodoApp.test.js
...
it('removes todo', () => {
const { getByText } = render(<TodoApp />);
const todoText = getByText('TDD 배우기');
const removeButton = todoText.nextSibling;
fireEvent.click(removeButton);
expect(todoText).not.toBeInTheDocument(); // 페이지에서 사라졌음을 의미
});
마지막으로 테스트케이스를 통과시켜보자.
TodoApp.js
import React, { useState, useCallback, useRef } from 'react';
import TodoItemList from './TodoItemList';
import TodoForm from './TodoForm';
const TodoApp = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: 'TDD 배우기',
done: true
},
{
id: 2,
text: 'react-testing-library 배우기',
done: true
}
]);
const nextId = useRef(3);
const onInsert = useCallback(
text => {
setTodos(
todos.concat({
id: nextId.current,
text,
done: false
})
);
nextId.current += 1;
},
[todos]
);
const onToggle = useCallback(
id => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
},
[todos]
);
const onRemove = useCallback(
id => {
setTodos(todos.filter(todo => todo.id !== id));
},
[todos]
);
return (
<>
<TodoForm onInsert={onInsert} />
<TodoItemList
todos={todos}
onToggle={onToggle}
onRemove={onRemove}
/>
</>
);
};
export default TodoApp;
이렇게 마지막 테스트케이스까지 통과에 성공하게 되면
터미널에 `$ yarn start
로 브라우저에 띄워 확인하면 된다.
스타일링은 건너뛰고 기능 구현에만 집중한 todolist가 아래와 같이 띄워지게 된다.
처음엔 아직도 기능구현에 어려움을 느끼기에..
테스트케이스를 먼저 작성해서
기능을 테스트 한다는 것 자체가 어렵게 다가왔지만,
막상 계속 반복해서 작업을 해보고,
하나하나 뜯어서 확인하니까 전체적인 틀에 대한 이해가 높아진 것 같다.
특히 내가 어려워하는 state나 props는 신경쓰지 않는다는 점에서
react-testing-library에 대해 조금은 호감도가 높아졌다(?)
물론 아직도 어떤 자료를 보지 않고 작성할 수 있는 수준은 절대 아니지만..
알지 못했던 부분에 대한 내용을 하나씩 늘려가는 것에 대해
조금씩 흥미를 느끼게 되는 것 같다.
하지만 여전히 터미널에 fail이 뜨는건 괜히 무섭고 떨린다ㅎ
계속해서 연습하고 연습해야지!