구성요소 테스트는 실용적인 테스트 코드를 보여주기 시작하는 좋은 위치입니다. 구성요소 테스트는 단순한 단위 테스트보다 실질적이고 엔드 투 엔드 테스트보다 덜 복잡하며 DOM과의 상호작용을 보여줍니다. 철학적으로 React를 사용하면 웹 개발자가 웹사이트 또는 웹 앱을 구성요소로 구성된 것으로 더 쉽게 생각할 수 있습니다.
따라서 개별 구성요소의 복잡도와 관계없이 이를 테스트하는 것이 신규 또는 기존 애플리케이션 테스트를 시작하는 좋은 방법입니다.
이 페이지에서는 복잡한 외부 종속 항목이 있는 소규모 구성요소 테스트를 안내합니다. 버튼을 클릭하고 숫자가 증가하는지 확인하는 등 다른 코드와 상호작용하지 않는 구성요소는 쉽게 테스트할 수 있습니다. 실제로 이러한 코드는 극소수이며 상호작용이 없는 테스트 코드의 가치는 제한적일 수 있습니다.
테스트 대상 구성요소
Vitest와 JSDOM 환경을 사용하여 React 구성요소를 테스트합니다. 이렇게 하면 브라우저를 에뮬레이션하는 동안 명령줄에서 Node를 사용하여 테스트를 빠르게 실행할 수 있습니다.
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
에서 가져와야 합니다.
하지만 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 구성요소를 테스트하는 데 어떤 접근 방식이 사용되었나요?