본문 바로가기

FrontEnd

리액트 테스트 : implementation details을 피하기

반응형

TLDR

 

  • 구현 세부사항과 관련없는 테스트
    • 사용자가 보는것처럼 테스트 
    • 마크업과 관련없는 유즈케이스 테스트
    • 스크린 유틸리티
    • 인터랙션 > 유저이벤트
    • 눈에 보이는 데이터 중 핵심에 초점

이전 포스트에서 사용자가 사용하는 방법

(렌더링 결과와 프롭) => (인터랙션) => (렌더링 결과와 프롭)

측면에서 테스트해야 함을 이해했습니다.

UI 측면의 테스트는 사용자가 보고, 듣고, 느끼는 것에 관한 것입니다.

테스트 할 컴포넌트

implementation details

"구현 세부사항"은 추상화가 특정 결과를 달성하는 방법(how)을 나타내는 용어입니다.
코드의 표현력 덕분에 완전히 다른 구현 세부 사항(how)을 사용하여 동일한 결과를 얻을 수 있습니다.
const multiply = (a, b) => a * b
// 는 아래와 같습니다.
function multiply(a, b) {
  let total = 0
  for (let i = 0; i < b; i++) {
    total += a
  }
  return total
}

첫번째 구현방법이 더 간결한 것은 전혀 우리의 관심사가 아닙니다.
추상화의 구현은 추상화 사용자에게 중요하지 않습니다.
(UI 유저는 심지어 리액트, 훅 등에 대해서도 관심이 없습니다.)
 

리팩터링을 통해 계속 동작한다는 확신 관점에서, 테스트도 마찬가지입니다.

리팩터링을 수행하는 개발자도 테스트가 어떻게 짜여져 있는지 관심이 없습니다.

구현 세부 사항은 (UI, API) 사용자가 신경 쓰지 않는 것입니다.

리액트 테스트 예제

아래와 같은 카운터 컴포넌트가 존재합니다.

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return <button onClick={increment}>{count}</button>
}

테스트 코드에서 버튼을 아래와 같이 접근할 수 있습니다.

const {container} = render(<Counter />)
container.firstChild // <-- that's the button

UI 개발자와 UI 사용자는 마크업에 관심이 없습니다. 오직 버튼에만 관심이 있습니다

아래와 같이 마크업을 바꾸었다고 테스트가 깨진다면, 이상할 겁니다.

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <span>
      <button onClick={increment}>{count}</button>
    </span>
  )
}

그럼 어떻게 테스트해야 할까요?

 

사용자가 화면을 보는 것처럼 테스트합니다.

  1. 사용자는 버튼을 찾습니다.
  2. 버튼의 초기값은 항상 0입니다.
  3. 해당 버튼을 클릭합니다.
render(<Counter />)
screen.getByText('0') // <-- that's the button
// or (even better) you can do this:
screen.getByRole('button', {name: '0'}) // <-- that's the button

즉, useCase(feature)와 관련된 부분을 테스트하면 됩니다.

즉 시스템 조작과 시스템 동작 결과(리액트의 경우 상태 반영 결과 => 렌더링 결과)입니다.

 

이전 게시물에서 작업한 내용에서 구현 세부사항을 걷어내 봅시다.


Screen Utility 활용

https://testing-library.com/docs/queries/about/#priority 문서를 참고하면, selector의 priority를 설명한 내용이 있습니다.

이 중 가장 우선하는 내용은, 눈에 보이는 것중에서도 Role 입니다.

접근성 탭의 role을 이용합시다.

import * as React from 'react'
import {render, screen, fireEvent} from '@testing-library/react'
import Counter from '../../components/counter'

test('counter increments and decrements when the buttons are clicked', () => {
  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)

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

브라우저 인터랙션 => UserEvent로 교체

결과적으로 이 버튼을 클릭하는 것도 구현 세부 사항입니다.
사용자가 실제로 여러 다른 이벤트를 실행해 때, 클릭이라는 하나의 이벤트를 실행하고 있습니다.
사용자가 버튼을 클릭하려면 먼저 버튼 위로 마우스를 움직여야 하며,
이때  다른 마우스 이벤트들이 발생합니다.
 
구현 세부 사항이 없는 구현을 원한다면 동일한 이벤트도 모두 발생시켜야 합니다.
운 좋게도 Testing Library는 @testing-library/user-event를 통해 해당 사항을 다루고 있습니다.
click method에서 해당 코드의 구현을 구경하세요.
import * as React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Counter from '../../components/counter'

test('counter increments and decrements when the buttons are clicked', () => {
  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)

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

더 나아가서

Current count : 숫자에서

더 중요한 메세지는 숫자일 것입니다.

카운트 : 0이건

ㅇㅁㄴㅇㅁㄴ : 0이건

사용자는 0에 더 관심을 둘것입니다.

소프트웨어를 사용하는 사람 입장에서 변하지 않는것(불변식)을 테스트 합시다.

더 공부하기

https://testing-library.com/docs/dom-testing-library/api-queries#screen

 

About Queries | Testing Library

Overview

testing-library.com

Testing Implementation Details (kentcdodds.com)

 

Testing Implementation Details

Testing implementation details is a recipe for disaster. Why is that? And what does it even mean?

kentcdodds.com

About Queries | Testing Library (testing-library.com)

 

About Queries | Testing Library

Overview

testing-library.com

Avoid the Test User (kentcdodds.com)

 

Avoid the Test User

How your UI code has only two users, but the wrong tests can add a third

kentcdodds.com

 

반응형