본문 바로가기

FrontEnd

8. 커스텀 훅 테스트

반응형

https://github.com/kentcdodds/testing-react-apps.git

TLDR : 훅은 구현상세다.

훅은 리액트 컴포넌트 내에서 호출해야한다.

즉, 훅의 동작은 컴포넌트의 상태 반영을 통해서만 테스트 가능하다.

사용자는 컴포넌트에만 관심있다.

즉 훅은 구현 상세이므로, 테스트를 지양해야 한다.

공통 훅을 만들어 라이브러리로 제공하는 경우가 아니면...

1. 가장 기본적인 테스트

딱히 구현을 보여주지 않아도 어떤 테스트인지 알 수 있다.

function UseCounterHookExample() {
  const {count, increment, decrement} = useCounter()
  return (
    <div>
      <div>Current count: {count}</div>
      <button onClick={decrement}>Decrement</button>
      <button onClick={increment}>Increment</button>
    </div>
  )
}

test('exposes the count and increment/decrement functions', () => {
  render(<UseCounterHookExample />)
  const increment = screen.getByRole('button', {name: /increment/i})
  const decrement = screen.getByRole('button', {name: /decrement/i})
  const message = screen.getByText(/current count/i)

  expect(message).toHaveTextContent('Current count: 0')
  userEvent.click(increment)
  expect(message).toHaveTextContent('Current count: 1')
  userEvent.click(decrement)
  expect(message).toHaveTextContent('Current count: 0')
})

2. 유틸리티를 통한 추상화

1. 먼저 null을 리턴하는 컴포넌트를 만든다.

그 다음, result를 레퍼런스하게 해보자. 아래 테스트는 통과한다.

act는 테스트 코드에서 컴포넌트 상태 변경을 트리거 하는 함수를 사용하면,

컴포넌트가 해당 사이드이펙트를 반영할 때까지 기다려야 함을 명시적으로 표현한다.

(userEvent나 fireEvent는 자동으로 감싸준다.)

test('exposes the count and increment/decrement functions', () => {
  let result
  function TestComponent() {
    result = useCounter()
    return null
  }
  render(<TestComponent />)
  expect(result.count).toBe(0)
  act(() => result.increment())
  expect(result.count).toBe(1)
  act(() => result.decrement())
  expect(result.count).toBe(0)
})

2. setup을 유틸로 분리해보자

아래에서 result.current가 아닌 result를 쓰게 되면?

test코드 내에서 result는 setUp할 때 한번만 할당된다. 

그 이후는 변경되지 않는다.

따라서 result내부의 레퍼런스를 통해, 계속 참조해야하는 객체가 필요하다.

function setup({initialProps} = {}) {
  const result = {}
  // 해당 함수는 render될 때마다 계속 호출된다.
  // current통해 새로 바뀐 객체의 참조를 가져온다.
  function TestComponent(props) {
    result.current = useCounter(props)
    return null
  }
  render(<TestComponent {...initialProps} />)
  return result
}

test('exposes the count and increment/decrement functions', () => {
  const result = setup()
  expect(result.current.count).toBe(0)
  act(() => result.current.increment())
  expect(result.current.count).toBe(1)
  act(() => result.current.decrement())
  expect(result.current.count).toBe(0)
})

test('allows customization of the initial count', () => {
  const result = setup({initialProps: {initialCount: 3}})
  expect(result.current.count).toBe(3)
})

test('allows customization of the step', () => {
  const result = setup({initialProps: {step: 2}})
  expect(result.current.count).toBe(0)
  act(() => result.current.increment())
  expect(result.current.count).toBe(2)
  act(() => result.current.decrement())
  expect(result.current.count).toBe(0)
})

3. react-hook-testing library

남이 만든 라이브러리를 가져다 쓰고 보일러플레이트 코드를 줄이자.

추가로 리렌더 메소드를 통해 설정을 변경할 수 있다.

import {renderHook, act} from '@testing-library/react-hooks'
import useCounter from '../../components/use-counter'

test('exposes the count and increment/decrement functions', () => {
  const {result} = renderHook(useCounter)
  expect(result.current.count).toBe(0)
  act(() => result.current.increment())
  expect(result.current.count).toBe(1)
  act(() => result.current.decrement())
  expect(result.current.count).toBe(0)
})

test('allows customization of the initial count', () => {
  const {result} = renderHook(useCounter, {initialProps: {initialCount: 3}})
  expect(result.current.count).toBe(3)
})

test('allows customization of the step', () => {
  const {result} = renderHook(useCounter, {initialProps: {step: 2}})
  expect(result.current.count).toBe(0)
  act(() => result.current.increment())
  expect(result.current.count).toBe(2)
  act(() => result.current.decrement())
  expect(result.current.count).toBe(0)
})

test('the step can be changed', () => {
  const {result, rerender} = renderHook(useCounter, {
    initialProps: {step: 3},
  })
  expect(result.current.count).toBe(0)
  act(() => result.current.increment())
  expect(result.current.count).toBe(3)
  rerender({step: 2})
  act(() => result.current.decrement())
  expect(result.current.count).toBe(1)
})
반응형