실제 구성요소 테스트

구성요소 테스트는 실용적인 테스트 코드를 보여주기 시작하는 좋은 위치입니다. 구성요소 테스트는 단순한 단위 테스트보다 실질적이고 엔드 투 엔드 테스트보다 덜 복잡하며 DOM과의 상호작용을 보여줍니다. 철학적으로 React를 사용하면 웹 개발자가 웹사이트 또는 웹 앱을 구성요소로 구성된 것으로 더 쉽게 생각할 수 있습니다.

따라서 개별 구성요소의 복잡도와 관계없이 이를 테스트하는 것이 신규 또는 기존 애플리케이션 테스트를 시작하는 좋은 방법입니다.

이 페이지에서는 복잡한 외부 종속 항목이 있는 소규모 구성요소 테스트를 안내합니다. 버튼을 클릭하고 숫자가 증가하는지 확인하는 등 다른 코드와 상호작용하지 않는 구성요소는 쉽게 테스트할 수 있습니다. 실제로 이러한 코드는 극소수이며 상호작용이 없는 테스트 코드의 가치는 제한적일 수 있습니다.

테스트 대상 구성요소

Vitest와 JSDOM 환경을 사용하여 React 구성요소를 테스트합니다. 이렇게 하면 브라우저를 에뮬레이션하는 동안 명령줄에서 Node를 사용하여 테스트를 빠르게 실행할 수 있습니다.

각 이름 옆에 선택 버튼이 있는 이름 목록
네트워크의 사용자 목록을 표시하는 작은 React 구성요소입니다.

UserList라는 이 React 구성요소는 네트워크에서 사용자 목록을 가져와 사용자가 그중 하나를 선택할 수 있도록 합니다. 사용자 목록은 useEffect 내에서 fetch를 사용하여 가져오고 선택 핸들러는 Context에 의해 전달됩니다. 코드는 다음과 같습니다.

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

이 예시는 React 권장사항을 보여주지 않습니다 (예: useEffect 내에서 fetch 사용). 하지만 코드베이스에는 이와 같은 사례가 많이 포함되어 있을 수 있습니다. 더 중요한 것은 이러한 케이스는 처음에는 테스트하기가 까다로워 보일 수 있다는 점입니다. 이 과정의 향후 섹션에서는 테스트 가능한 코드 작성에 대해 자세히 설명합니다.

이 예에서는 다음을 테스트합니다.

  • 네트워크의 데이터에 대한 응답으로 올바른 DOM이 생성되는지 확인합니다.
  • 사용자를 클릭하면 콜백이 트리거되는지 확인합니다.

모든 구성요소는 각기 다릅니다. 이 테스트가 흥미로운 이유는 무엇인가요?

  • 전역 fetch를 사용하여 네트워크에서 실제 데이터를 요청합니다. 이 데이터는 테스트 중에 불안정하거나 느릴 수 있습니다.
  • 암시적으로 테스트하고 싶지 않은 다른 클래스 UserRow를 가져옵니다.
  • 테스트 대상 코드의 일부가 아닌 Context를 사용하며 일반적으로 상위 구성요소에서 제공합니다.

간단한 테스트를 작성하여 시작합니다.

이 구성요소에 관한 매우 기본적인 사항을 빠르게 테스트할 수 있습니다. 분명히 말씀드리자면 이 예시가 그리 유용하지는 않습니다. 하지만 UserList.test.tsx라는 피어 파일에서 템플릿을 설정하는 것이 좋습니다. Vitest와 같은 테스트 실행기는 기본적으로 .test.js로 끝나는 파일 또는 .tsx를 포함한 유사한 파일을 실행합니다.

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

이 테스트는 구성요소가 렌더링될 때 '사용자'라는 텍스트가 포함된다고 어설션합니다. 구성요소에 네트워크에 fetch를 전송하는 부작용이 있음에도 작동합니다. 테스트가 종료되었을 때 fetch가 아직 진행 중이며 설정된 엔드포인트가 없습니다. 적어도 제한 시간을 기다리지 않고는 테스트가 종료될 때 사용자 정보가 표시되는지 확인할 수 없습니다.

예시 fetch()

모의 처리는 테스트를 위해 실제 함수나 클래스를 테스트에서 제어할 수 있는 것으로 대체하는 작업입니다. 이는 가장 간단한 단위 테스트를 제외한 거의 모든 유형의 테스트에서 일반적인 관행입니다. 자세한 내용은 어설션 및 기타 프리미티브를 참고하세요.

테스트가 빠르게 완료되고 '실제' 또는 알 수 없는 데이터가 아닌 예상한 데이터를 반환하도록 테스트의 fetch()를 모의 처리할 수 있습니다. fetch전역이므로 코드에 import하거나 require할 필요가 없습니다.

vitest에서는 vi.fn()에서 반환된 특수 객체로 vi.stubGlobal를 호출하여 전역 객체를 모의 처리할 수 있습니다. 이렇게 하면 나중에 수정할 수 있는 모의 객체가 빌드됩니다. 이러한 메서드는 이 과정의 후반부 섹션에서 더 자세히 살펴보겠지만 다음 코드에서 실제로 확인할 수 있습니다.

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

이 코드는 모의 항목을 추가하고, 네트워크 가져오기 Response의 가짜 버전을 설명한 후 표시될 때까지 기다립니다. 텍스트가 표시되지 않으면(queryByText의 쿼리를 새 이름으로 변경하여 확인할 수 있음) 테스트가 실패합니다.

이 예에서는 Vitest의 내장 모의 처리 도우미를 사용했지만 다른 테스트 프레임워크에도 모의 처리와 유사한 접근 방식이 있습니다. Vitest는 모든 테스트 후에 vi.unstubAllGlobals()를 호출하거나 동등한 전역 옵션을 설정해야 한다는 점에서 고유합니다. 작업을 '실행취소'하지 않으면 fetch 모의가 다른 테스트에 영향을 미칠 수 있으며 모든 요청에 이상한 JSON 덩어리가 응답으로 제공됩니다.

가상 가져오기

UserList 구성요소 자체가 UserRow라는 구성요소를 가져오는 것을 확인했을 수 있습니다. 코드는 포함하지 않았지만 사용자 이름을 렌더링하는 것을 볼 수 있습니다. 이전 테스트에서는 '샘'을 확인하며 이는 UserList 내에서 직접 렌더링되지 않으므로 UserRow에서 가져와야 합니다.

사용자 이름이 구성요소를 통해 이동하는 방식을 보여주는 플로우 차트
UserListTestUserRow의 공개 상태를 알 수 없습니다.

하지만 UserRow 자체가 복잡한 구성요소일 수 있습니다. 추가 사용자 데이터를 가져오거나 테스트와 관련 없는 부작용이 있을 수 있습니다. 이러한 가변성을 제거하면 테스트가 더 유용해집니다. 특히 테스트하려는 구성요소가 더 복잡해지고 종속 항목과 더 얽히게 될 때 유용합니다.

다행히 Vitest를 사용하면 테스트에서 직접 가져오지 않더라도 특정 가져오기를 모의 처리하여 이를 사용하는 모든 코드에 간단한 버전 또는 알려진 버전이 제공되도록 할 수 있습니다.

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

fetch 전역을 모의 처리하는 것처럼 이는 강력한 도구이지만 코드에 종속 항목이 많으면 지속 가능하지 않을 수 있습니다. 다시 한번 강조하지만 테스트 가능한 코드를 작성하는 것이 가장 좋습니다.

클릭하여 컨텍스트 제공

React 및 Lit과 같은 기타 라이브러리에는 Context라는 개념이 있습니다. 샘플 코드에는 사용자가 선택되면 메서드를 호출하는 UserContext가 포함되어 있습니다. 이는 콜백이 UserList에 직접 전달되는 '속성 드릴링'의 대안으로 간주되는 경우가 많습니다.

테스트 하네스가 UserContext를 제공하지 않았습니다. 이를 사용하지 않고 React 테스트에 클릭 작업을 추가하면 최악의 경우 테스트가 비정상 종료될 수 있습니다. 기본 인스턴스가 다른 위치에 제공된 경우 최선의 경우에도 제어할 수 없는 동작이 발생할 수 있습니다 (위의 알 수 없는 UserRow와 유사).

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

대신 구성요소를 렌더링할 때 자체 Context를 제공할 수 있습니다. 이 예에서는 호출이 이루어졌는지, 어떤 인수를 사용했는지 확인하는 데 사용할 수 있는 Vitest 모의 함수vi.fn() 인스턴스를 사용합니다.

이 경우 이전 예시에서 모의 처리된 fetch와 상호작용하며 테스트에서 전달된 ID가 sam인지 확인할 수 있습니다.

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

이는 테스트하려는 핵심 구성요소에서 관련 없는 종속 항목을 삭제할 수 있는 간단하지만 강력한 패턴입니다.

요약

이 예에서는 테스트하기 어려운 React 구성요소를 테스트하고 보호하기 위한 구성요소 테스트를 빌드하는 방법을 보여줍니다. 이 테스트에서는 구성요소가 종속 항목(fetch 전역, 가져온 하위 구성요소, Context)과 올바르게 상호작용하는지 확인하는 데 중점을 두었습니다.

이해도 확인

React 구성요소를 테스트하는 데 어떤 접근 방식이 사용되었나요?

컨텍스트를 사용한 종속 항목 삽입
숫자가 증가했는지 확인
테스트를 위해 복잡한 종속 항목을 간단한 종속 항목으로 모의 처리
스터빙 전역 변수