이제는 작성하자 테스트 코드!
테스트 코드를 작성할 때 보통 React Testing Library
나 Jest
를 사용한다. 아무 의심 없이 라이브러리에서 제공하는 메서드를 사용해서 테스트 코드를 작성했는데 이 라이브러리를 왜 써야 하는지에 대해 알아야할 것 같아 정리한다
위와 같은 간단한 카운트 버튼 컴포넌트에 대해 테스트 코드를 작성한다고 가정하다. 테스트에 필요한 것은
이렇게 3가지 정도가 되겠다. 이를 라이브러리 없이 작성해보자
test('without dispatchEvent, counter increments and decrements when the buttons are clicked', () => {
const div = document.createElement('div')
document.body.append('div')
})
먼저 테스트를 진행할 dom이 필요하다(도화지 처럼) div를 만들고 body에 append해준다.
test('without dispatchEvent, counter increments and decrements when the buttons are clicked', () => {
// div 생성
const div = document.createElement('div')
document.body.append('div')
// 테스트 컴포넌트 렌더링
const root = createRoot(div)
act(() => root.render(<Counter />))
// Counter 컴포넌트 내 돔 태그가 잘 가져오는지 확인
const msg = div.firstChild.querySelector('div')
expect(msg.textContent).toBe('Current count: 0')
})
그리고 ReactDom에 테스트할 컴포넌트 렌더링한다. 첫 테스트는 통과.
이제 버튼을 가져와서 increment 버튼을 누른다.
test('without dispatchEvent, counter increments and decrements when the buttons are clicked', () => {
...
// 버튼을 가져온다
const [decrement, increment] = div.querySelectorAll('button')
// act로 맵핑해야 가상돔에 반영된다.
act(() => increment.click())
// 현재 count는 1이므로 아래는 실패다
expect(msg.textContent).toBe('Current count: 0')
})
그럼 이제 증감에 따라 버튼이 잘 작동하는지 테스트한다.
test('without dispatchEvent, counter increments and decrements when the buttons are clicked', () => {
...
act(() => increment.click())
expect(msg.textContent).toBe('Current count: 1')
act(() => decrement.click())
expect(msg.textContent).toBe('Current count: 0')
})
성공이다.
이제 카운트 컴포넌트 관련 두 번째 테스트 케이스를 작성한다고 가정해보자.
test('두 번째 테스트 케이스', () => {
// 두 번째 테스트 케이스 body엔 div를 append하지 않고 log를 찍어보면
console.log(document.body.innerHTML)
})
div가 있는 걸로 나온다. 첫 번째 케이스에 의존하고 있어서 그렇다. 그래서 테스트 케이스는 각각 독립적으로 테스트 해야한다.
라이브러리를 사용하지 않는 다면 테스트 케이스가 끝날 때마다
div.remove()
로 돔을 지워줘야 한다. 하지만 문제는 remove() 라인 전에 테스트가 실패하면 다른 테스트 케이스들도 실패로 간주된다.
그래서 테스트 최상위에
beforeEach(() => {
document.body.innerHTML = ''
})
를 선언해줘서 독립적으로 만들어야 한다.
테스트코드를 작성할 때 button.click()
도 가능하지만 좀 더 섬세하게 버튼을 다룰 필요가 있다(mouseHover 등 다른 이벤트) 이때 dispatch event
를 사용한다.
위에 작성했던 테스트 코드 click() 부분을 dispatch event로 변경하면
test('counter increments and decrements when the buttons are clicked', () => {
const div = document.createElement('div')
document.body.append('div')
const root = createRoot(div)
act(() => root.render(<Counter />))
const [decrement, increment] = div.querySelectorAll('button')
const msg = div.firstChild.querySelector('div')
expect(msg.textContent).toBe('Current count: 0')
// MouseEvent 객체를 만든다.
const incrementButton = new MouseEvent('click', {
bubbles: true,
cancelable: true,
// left click임
button: 0,
})
// act에 맵핑하여 이벤트 실행
act(() => increment.dispatchEvent(incrementButton))
expect(msg.textContent).toBe('Current count: 1')
})
통과 !
그럼 이제 위 테스트 코드을 React Testing Library(이하 RTL)에서 어떻게 사용하는지 확인해보자.
먼저 RTL에서 제공하는 render
를 통해 테스트 컴포넌트를 렌더링 할 수 있다.
test('with rtl counter increments and decrements when the buttons are clicked', () => {
// render를 사용하면 cleanup을 자동으로 해준다.
const {container} = render(<Counter />)
})
div 만들고 body에 append하고 dom에 렌더링했던 코드를 모두 작성하지 않아도 된다.
이제 event 부분도 수정해보자. RTL에서 제공하는 fireEvent
로 dispatch event를 대체할 수 있다.
test('with rtl counter increments and decrements when the buttons are clicked', () => {
// render를 사용하면 cleanup을 자동으로 해준다.
const {container} = render(<Counter />)
const [decrement, increment] = container.querySelectorAll('button')
const message = container.firstChild.querySelector('div')
// fireEvent를 사용하면 act로 맵핑하지 않아도 된다.
expect(message).toHaveTextContent('Current count: 0')
fireEvent.click(increment)
expect(message).toHaveTextContent('Current count: 1')
fireEvent.click(decrement)
expect(message).toHaveTextContent('Current count: 0')
})
통과 !
다음으로 돔 타켓팅도 RTL에서 제공하는 메서드를 사용해서 테스트 코드를 깔끔하게 만들자.
screen
을 사용하면 돔에서 원하는 태그를 쉽게 가져올 수 있다.
왜 screen을 사용해야 하는지에 대한 설명
test('counter increments and decrements when the buttons are clicked', async () => {
render(<Counter />)
const increment = screen.getByRole('button', {name: /increment/i})
const decrement = screen.getByRole('button', {name: /decrement/i})
const message = screen.getByText(/current count/i)
})
screen
에서 제공하는 getByRole
과 getByText
로 정확한 태그를 가져온다.
test('counter increments and decrements when the buttons are clicked', async () => {
render(<Counter />)
const increment = screen.getByRole('button', {name: /increment/i})
const decrement = screen.getByRole('button', {name: /decrement/i})
const message = screen.getByText(/current count/i)
// fireEvent <-> act
// fireEvent는 onClick을 호출하지만, userEvent는 실제로 클릭을 한다.
expect(message).toHaveTextContent('Current count: 0')
await userEvent.click(increment)
expect(message).toHaveTextContent('Current count: 1')
await userEvent.click(decrement)
expect(message).toHaveTextContent('Current count: 0')
})
fireEvent
보다 userEvent
를 사용해야한다. 가령 button에 onClick이 아니라 onMouseDown을 줄 경우 테스트가 broken되기 때문
통과 !
** kent.c.dodds의 리액트 테스팅 강의를 학습하고 씀