실제 구성요소 테스트

구성요소 테스트는 실용적인 테스트 코드를 보여주기에 좋은 위치입니다. 구성요소 테스트는 간단한 단위 테스트보다 더 크고, 엔드 투 엔드 테스트보다 덜 복잡하며, 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와 같은 테스트 실행기는 기본적으로 .tsx를 포함하여 .test.js 또는 이와 유사한 이름으로 끝나는 파일을 실행합니다.

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);
});

이 테스트는 구성요소가 렌더링될 때 'Users'라는 텍스트를 포함한다고 어설션합니다. 구성요소에 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라는 구성요소를 가져오는 것을 알 수 있습니다. 코드는 포함되어 있지 않지만 사용자 이름이 렌더링되는 것을 확인할 수 있습니다. 이전 테스트는 'Sam'을 확인하는데 이는 UserList 내에서 직접 렌더링되지 않으므로 UserRow에서 가져와야 합니다.

구성요소에서 사용자 이름이 이동하는 방식을 보여주는 플로우 차트
UserListTest에는 UserRow의 공개 상태가 없습니다.

하지만 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를 제공할 수 있습니다. 이 예에서는 vi.fn()의 인스턴스인 Vitest Mock 함수를 사용합니다. 이 함수는 호출이 이루어졌는지와 어떤 인수로 호출되었는지 확인하기 위해 팩트 후에 사용할 수 있습니다. 여기서는 앞의 예에서 모의 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 구성요소를 테스트하는 데 어떤 접근 방식을 사용했나요?

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