React Testing (1)

김동하·2023년 12월 28일
0

jest

목록 보기
5/6
post-custom-banner

들어가며

이제는 작성하자 테스트 코드!

기본 예제

테스트 코드를 작성할 때 보통 React Testing LibraryJest 를 사용한다. 아무 의심 없이 라이브러리에서 제공하는 메서드를 사용해서 테스트 코드를 작성했는데 이 라이브러리를 왜 써야 하는지에 대해 알아야할 것 같아 정리한다

위와 같은 간단한 카운트 버튼 컴포넌트에 대해 테스트 코드를 작성한다고 가정하다. 테스트에 필요한 것은

  • 현재 카운트가 나오는 태그
  • 증감과 관련된 버튼
  • 증감 버튼을 클릭했을 때 현재 카운트가 일치하는지

이렇게 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 = ''
})

를 선언해줘서 독립적으로 만들어야 한다.

dispatch event

테스트코드를 작성할 때 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

그럼 이제 위 테스트 코드을 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에서 제공하는 getByRolegetByText로 정확한 태그를 가져온다.

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의 리액트 테스팅 강의를 학습하고 씀

profile
프론트엔드 개발
post-custom-banner

0개의 댓글