React Hooks 테스트를 위한 완벽한 가이드

게시 됨: 2022-03-11

후크는 2018년 후반에 React 16.8에 도입되었습니다. 후크는 기능적 구성 요소에 연결되어 componentDidUpdate , componentDidMount 등과 같은 상태 및 구성 요소 기능을 사용할 수 있게 해주는 기능입니다. 이전에는 불가능했습니다.

또한 후크를 사용하면 다른 구성 요소에서 구성 요소 및 상태 논리를 재사용할 수 있습니다. 이것은 이전에 하기 까다로웠습니다. 따라서 후크는 게임 체인저였습니다.

이 기사에서는 React Hooks를 테스트하는 방법에 대해 알아볼 것입니다. 우리는 충분히 복잡한 후크를 선택하고 테스트할 것입니다.

우리는 당신이 이미 React Hooks에 익숙한 열렬한 React 개발자이기를 기대합니다. 지식을 다듬고 싶다면 자습서를 확인해야 하며 여기에 공식 문서에 대한 링크가 있습니다.

테스트에 사용할 후크

이 기사에서는 이전 기사인 Stale-while-revalidate Data Fetching with React Hooks에서 작성한 후크를 사용합니다. 후크는 useStaleRefresh 라고 합니다. 기사를 읽지 않은 경우 여기에서 해당 부분을 요약할 것이므로 걱정하지 마십시오.

테스트할 후크는 다음과 같습니다.

 import { useState, useEffect } from "react"; const CACHE = {}; export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); setData(defaultValue); } // fetch new data fetch(url) .then((res) => res.json()) .then((newData) => { CACHE[cacheID] = newData; setData(newData); setLoading(false); }); }, [url, defaultValue]); return [data, isLoading]; }

보시다시피 useStaleRefresh 는 데이터의 캐시된 버전이 있는 경우 이를 반환하면서 URL에서 데이터를 가져오는 데 도움이 되는 후크입니다. 간단한 인메모리 저장소를 사용하여 캐시를 보유합니다.

또한 아직 사용할 수 있는 데이터나 캐시가 없는 경우 true인 isLoading 값을 반환합니다. 클라이언트는 이를 사용하여 로딩 표시기를 표시할 수 있습니다. 캐시 또는 새로운 응답을 사용할 수 있는 경우 isLoading 값은 false로 설정됩니다.

stale-while-refresh 로직을 추적하는 순서도

이 시점에서 위의 후크를 읽는 데 시간을 할애하여 그것이 하는 일을 완전히 이해하는 것이 좋습니다.

이 기사에서는 먼저 테스트 라이브러리를 사용하지 않고(React Test Utilities 및 Jest만) react-hooks-testing-library를 사용하여 이 후크를 테스트하는 방법을 볼 것입니다.

테스트 라이브러리를 사용하지 않는 이유, 즉 테스트 Jest 만 사용하는 이유는 후크 테스트가 어떻게 작동하는지 보여주기 위함입니다. 이러한 지식을 바탕으로 테스트 추상화를 제공하는 라이브러리를 사용할 때 발생할 수 있는 모든 문제를 디버깅할 수 있습니다.

테스트 케이스 정의

이 후크 테스트를 시작하기 전에 테스트할 계획을 세워봅시다. 후크가 무엇을 해야 하는지 알고 있으므로 테스트를 위한 8단계 계획은 다음과 같습니다.

  1. 후크가 URL url1 로 마운트되면 isLoadingtrue 이고 데이터는 defaultValue 입니다.
  2. 비동기 가져오기 요청 후 후크는 데이터 data1 로 업데이트되고 isLoadingfalse 입니다.
  3. URL이 url2 로 변경되면 isLoading 은 다시 true가 되고 data는 defaultValue 가 됩니다.
  4. 비동기 가져오기 요청 후 후크는 새 데이터 data2 로 업데이트됩니다.
  5. 그런 다음 URL을 다시 url1 로 변경합니다. 데이터 data1 은 캐시된 이후 즉시 수신됩니다. isLoading 은 거짓입니다.
  6. 비동기 페치 요청 후 새로운 응답이 수신되면 데이터가 data3 으로 업데이트됩니다.
  7. 그런 다음 URL을 다시 url2 로 변경합니다. 데이터 data2 는 캐시된 이후 즉시 수신됩니다. isLoading 은 거짓입니다.
  8. 비동기 가져오기 요청 후 새로운 응답이 수신되면 데이터가 data4 로 업데이트됩니다.

위에서 언급한 테스트 흐름은 후크가 작동하는 방식의 궤적을 명확하게 정의합니다. 따라서 이 테스트가 작동하는지 확인할 수 있다면 우리는 좋은 것입니다.

테스트 흐름

라이브러리 없이 후크 테스트

이 섹션에서는 라이브러리를 사용하지 않고 후크를 테스트하는 방법을 살펴보겠습니다. 이것은 React Hooks를 테스트하는 방법에 대한 심층적인 이해를 제공합니다.

이 테스트를 시작하려면 먼저 mock fetch 를 하고 싶습니다. 이는 API가 반환하는 내용을 제어할 수 있도록 하기 위한 것입니다. 다음은 조롱된 fetch 입니다.

 function fetchMock(url, suffix = "") { return new Promise((resolve) => setTimeout(() => { resolve({ json: () => Promise.resolve({ data: url + suffix, }), }); }, 200 + Math.random() * 300) ); }

이 수정된 fetch 는 응답 유형이 항상 JSON이고 기본적으로 매개변수 urldata 값으로 반환한다고 가정합니다. 또한 응답에 200ms에서 500ms 사이의 임의 지연을 추가합니다.

응답을 변경하려면 두 번째 인수 suffix 를 비어 있지 않은 문자열 값으로 설정하기만 하면 됩니다.

이 시점에서 왜 지연이 되는지 물을 수 있습니다. 응답을 즉시 반환하지 않는 이유는 무엇입니까? 가능한 한 현실 세계를 복제하고 싶기 때문입니다. 즉시 반환하면 후크를 올바르게 테스트할 수 없습니다. 물론 더 빠른 테스트를 위해 지연을 50-100ms로 줄일 수 있지만 이 기사에서는 이에 대해 걱정하지 말자.

가져오기 모의가 준비되면 fetch 기능으로 설정할 수 있습니다. 이 기능은 상태 비저장이므로 개별 테스트 후에 재설정할 필요가 없기 때문에 beforeAllafterAll 을 사용합니다.

 // runs before any tests start running beforeAll(() => { jest.spyOn(global, "fetch").mockImplementation(fetchMock); }); // runs after all tests have finished afterAll(() => { global.fetch.mockClear(); });

그런 다음 구성 요소에 후크를 마운트해야 합니다. 왜요? 후크는 그 자체로 기능일 뿐입니다. 구성 요소에서 사용될 때만 useState , useEffect 등에 응답할 수 있습니다.

따라서 후크를 마운트하는 데 도움이 되는 TestComponent 를 만들어야 합니다.

 // defaultValue is a global variable to avoid changing the object pointer on re-render // we can also deep compare `defaultValue` inside the hook's useEffect const defaultValue = { data: "" }; function TestComponent({ url }) { const [data, isLoading] = useStaleRefresh(url, defaultValue); if (isLoading) { return <div>loading</div>; } return <div>{data.data}</div>; }

이것은 데이터를 렌더링하거나 데이터가 로드 중(가져오기 중)인 경우 "로드 중" 텍스트 프롬프트를 렌더링하는 간단한 구성 요소입니다.

테스트 구성 요소가 있으면 DOM에 마운트해야 합니다. 우리는 각 테스트 전에 새로운 DOM으로 시작하기를 원하기 때문에 각 테스트에 대해 구성 요소를 마운트 및 마운트 해제하기 위해 beforeEachafterEach 를 사용합니다.

 let container = null; beforeEach(() => { // set up a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); }); afterEach(() => { // cleanup on exiting unmountComponentAtNode(container); container.remove(); container = null; });

테스트 어설션을 위해 컨테이너에 액세스하기를 원하기 때문에 container 는 전역 변수여야 합니다.

이 세트를 사용하여 URL url1 을 렌더링하는 첫 번째 테스트를 하고 URL을 가져오는 데 시간이 걸리므로( fetchMock 참조) 처음에 "로드" 텍스트를 렌더링해야 합니다.

 it("useStaleRefresh hook runs correctly", () => { act(() => { render(<TestComponent url="url1" />, container); }); expect(container.textContent).toBe("loading"); })

yarn test 를 사용하여 테스트를 실행하면 예상대로 작동합니다. 다음은 GitHub의 전체 코드입니다.

이제 이 loading 텍스트가 가져온 응답 데이터 url1 로 언제 변경되는지 테스트해 보겠습니다.

어떻게 합니까? fetchMock 을 보면 200-500밀리초를 기다리는 것을 볼 수 있습니다. 500밀리초를 기다리는 테스트에 sleep 를 넣으면 어떻게 될까요? 가능한 모든 대기 시간을 다룹니다. 시도해 봅시다.

 function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } it("useStaleRefresh hook runs correctly", async () => { act(() => { render(<TestComponent url="url1" />, container); }); expect(container.textContent).toBe("loading"); await sleep(500); expect(container.textContent).toBe("url1"); });

테스트는 통과했지만 오류(코드)도 표시됩니다.

 PASS src/useStaleRefresh.test.js ✓ useStaleRefresh hook runs correctly (519ms) console.error node_modules/react-dom/cjs/react-dom.development.js:88 Warning: An update to TestComponent inside a test was not wrapped in act(...).

useStaleRefresh 후크의 상태 업데이트가 act() 외부에서 발생하기 때문입니다. DOM 업데이트가 적시에 처리되도록 React는 재렌더링 또는 UI 업데이트가 발생할 수 있을 때마다 act() 를 사용할 것을 권장합니다. 따라서 상태 업데이트가 발생하는 시간이므로 수면을 act 로 래핑해야 합니다. 그렇게하면 오류가 사라집니다.

 import { act } from "react-dom/test-utils"; // ... await act(() => sleep(500));

이제 다시 실행하십시오(GitHub의 코드). 예상대로 오류 없이 통과합니다.

먼저 URL을 url2 로 변경한 다음 loading 화면을 확인한 다음 fetch 응답을 기다리고 마지막으로 url2 텍스트를 확인하는 다음 상황을 테스트해 보겠습니다. 이제 비동기 변경을 올바르게 기다리는 방법을 알고 있으므로 이것은 쉬울 것입니다.

 act(() => { render(<TestComponent url="url2" />, container); }); expect(container.textContent).toContain("loading"); await act(() => sleep(500)); expect(container.textContent).toBe("url2");

이 테스트를 실행하면 역시 통과합니다. 이제 응답 데이터가 변경되고 캐시가 작동하는 경우도 테스트할 수 있습니다.

fetchMock 함수에 추가 인수 suffix 가 있음을 알 수 있습니다. 응답 데이터를 변경하기 위한 것입니다. 그래서 우리는 suffix 를 사용하도록 fetch mock을 업데이트합니다.

 global.fetch.mockImplementation((url) => fetchMock(url, "__"));

이제 URL이 url1 로 설정된 경우를 다시 테스트할 수 있습니다. 먼저 url1 을 로드한 다음 url1__ 을 로드합니다. url2 에 대해서도 동일한 작업을 수행할 수 있으며 놀라움이 없어야 합니다.

 it("useStaleRefresh hook runs correctly", async () => { // ... // new response global.fetch.mockImplementation((url) => fetchMock(url, "__")); // set url to url1 again act(() => { render(<TestComponent url="url1" />, container); }); expect(container.textContent).toBe("url1"); await act(() => sleep(500)); expect(container.textContent).toBe("url1__"); // set url to url2 again act(() => { render(<TestComponent url="url2" />, container); }); expect(container.textContent).toBe("url2"); await act(() => sleep(500)); expect(container.textContent).toBe("url2__"); });

이 전체 테스트는 후크가 실제로 예상대로 작동한다는 확신을 줍니다(코드). 만세! 이제 도우미 메서드를 사용한 최적화에 대해 간단히 살펴보겠습니다.

도우미 메서드를 사용하여 테스트 최적화하기

지금까지 후크를 완전히 테스트하는 방법을 살펴보았습니다. 접근 방식이 완벽하지는 않지만 작동합니다. 하지만 더 잘할 수 있습니까?

네. 각 가져오기가 완료될 때까지 고정된 500ms를 기다리고 있지만 각 요청에는 200~500ms가 걸립니다. 그래서 우리는 분명히 여기서 시간을 낭비하고 있습니다. 각 요청에 걸리는 시간을 기다리면 이 문제를 더 잘 처리할 수 있습니다.

어떻게 합니까? 간단한 기술은 어설션이 통과하거나 시간 초과에 도달할 때까지 어설션을 실행하는 것입니다. 이를 수행하는 waitFor 함수를 만들어 보겠습니다.

 async function waitFor(cb, timeout = 500) { const step = 10; let timeSpent = 0; let timedOut = false; while (true) { try { await sleep(step); timeSpent += step; cb(); break; } catch {} if (timeSpent >= timeout) { timedOut = true; break; } } if (timedOut) { throw new Error("timeout"); } }

이 함수는 단순히 try...catch 블록 내에서 10ms마다 콜백(cb)을 실행하고 timeout 에 도달하면 오류가 발생합니다. 이것은 우리가 안전한 방식으로 통과할 때까지(즉, 무한 루프가 없을 때까지) 주장을 실행할 수 있도록 합니다.

테스트에서 다음과 같이 사용할 수 있습니다. 500ms 동안 휴면 상태를 유지한 다음 어설션하는 대신 waitFor 함수를 사용합니다.

 // INSTEAD OF await act(() => sleep(500)); expect(container.textContent).toBe("url1"); // WE DO await act(() => waitFor(() => { expect(container.textContent).toBe("url1"); }) );

이러한 모든 주장에서 이를 수행하면 테스트 실행(코드) 속도에서 상당한 차이를 볼 수 있습니다.

이제 이 모든 것이 훌륭하지만 UI를 통해 후크를 테스트하고 싶지 않을 수도 있습니다. 반환 값을 사용하여 후크를 테스트하고 싶을 수도 있습니다. 어떻게 합니까?

우리는 이미 후크의 반환 값에 액세스할 수 있기 때문에 어렵지 않습니다. 구성 요소 내부에 있습니다. 이러한 변수를 전역 범위로 가져올 수 있다면 작동할 것입니다. 그렇게 합시다.

렌더링된 DOM이 아닌 반환 값을 통해 후크를 테스트할 것이므로 구성 요소에서 HTML 렌더링을 제거하고 null 로 렌더링할 수 있습니다. 우리는 또한 후크의 반환에서 구조화를 제거하여 더 일반적으로 만들어야 합니다. 따라서 이 업데이트된 테스트 구성 요소가 있습니다.

 // global variable let result; function TestComponent({ url }) { result = useStaleRefresh(url, defaultValue); return null; }

이제 후크의 반환 값은 전역 변수인 result 에 저장됩니다. 주장에 대해 쿼리할 수 있습니다.

 // INSTEAD OF expect(container.textContent).toContain("loading"); // WE DO expect(result[1]).toBe(true); // INSTEAD OF expect(container.textContent).toBe("url1"); // WE DO expect(result[0].data).toBe("url1");

모든 곳에서 변경한 후 테스트가 통과하는 것을 볼 수 있습니다(코드).

이 시점에서 우리는 React Hooks 테스트의 요지를 얻습니다. 다음과 같이 아직 개선할 수 있는 몇 가지 사항이 있습니다.

  1. result 변수를 로컬 범위로 이동
  2. 테스트하려는 모든 후크에 대한 구성 요소를 생성할 필요 제거

내부에 테스트 구성 요소가 있는 팩토리 함수를 만들어 이를 수행할 수 있습니다. 또한 테스트 구성 요소의 후크를 렌더링하고 result 변수에 대한 액세스 권한을 제공해야 합니다. 어떻게 할 수 있는지 봅시다.

먼저 TestComponentresult 를 함수 내부로 이동합니다. 또한 테스트 구성 요소에서 사용할 수 있도록 Hook 및 Hook 인수를 함수의 인수로 전달해야 합니다. 그것을 사용하여 여기 우리가 가진 것이 있습니다. 우리는 이 함수 renderHook 이라고 부릅니다.

 function renderHook(hook, args) { let result = {}; function TestComponent({ hookArgs }) { result.current = hook(...hookArgs); return null; } act(() => { render(<TestComponent hookArgs={args} />, container); }); return result; }

result.current 에 데이터를 저장하는 객체로 result 가 있는 이유는 테스트가 실행될 때 반환 값이 업데이트되기를 원하기 때문입니다. 후크의 반환 값은 배열이므로 직접 반환하면 값으로 복사됩니다. 객체에 저장하여 해당 객체에 대한 참조를 반환하므로 result.current 를 업데이트하여 반환 값을 업데이트할 수 있습니다.

이제 후크를 업데이트하는 방법은 무엇입니까? 우리는 이미 클로저를 사용하고 있기 때문에 그것을 할 수 있는 또 다른 rerender 함수를 둘러싸겠습니다.

최종 renderHook 기능은 다음과 같습니다.

 function renderHook(hook, args) { let result = {}; function TestComponent({ hookArgs }) { result.current = hook(...hookArgs); return null; } function rerender(args) { act(() => { render(<TestComponent hookArgs={args} />, container); }); } rerender(args); return { result, rerender }; }

이제 테스트에서 사용할 수 있습니다. actrender 를 사용하는 대신 다음을 수행합니다.

 const { rerender, result } = renderHook(useStaleRefresh, [ "url1", defaultValue, ]);

그런 다음 rerender 를 사용하여 주장하고 result.current 를 사용하여 후크를 업데이트할 수 있습니다. 다음은 간단한 예입니다.

 rerender(["url2", defaultValue]); expect(result.current[1]).toBe(true); // check isLoading is true

모든 곳에서 변경하면 문제(코드) 없이 작동하는 것을 볼 수 있습니다.

멋진! 이제 우리는 후크를 테스트하기 위한 훨씬 더 깔끔한 추상화를 갖게 되었습니다. 우리는 여전히 더 잘할 수 있습니다. 예를 들어 defaultValue 는 변경되지 않더라도 다시 rerender 할 때마다 매번 전달되어야 합니다. 우리는 그것을 고칠 수 있습니다.

그러나 우리는 이미 이 경험을 크게 향상시키는 라이브러리를 가지고 있으므로 수풀을 너무 헤매지 말자.

react-hooks-testing-library를 입력합니다.

React-hooks-testing-library를 사용한 테스트

React-hooks-testing-library는 우리가 이전에 이야기한 모든 작업을 수행한 다음 일부 작업을 수행합니다. 예를 들어 컨테이너 마운트 및 마운트 해제를 처리하므로 테스트 파일에서 수행할 필요가 없습니다. 이를 통해 산만하지 않고 후크 테스트에 집중할 수 있습니다.

rerenderresult 를 반환하는 renderHook 함수와 함께 제공됩니다. 또한 waitFor 와 유사한 wait 를 반환하므로 직접 구현할 필요가 없습니다.

다음은 React-hooks-testing-library에서 후크를 렌더링하는 방법입니다. 후크는 콜백 형식으로 전달됩니다. 이 콜백은 테스트 구성 요소가 다시 렌더링될 때마다 실행됩니다.

 const { result, wait, rerender } = renderHook( ({ url }) => useStaleRefresh(url, defaultValue), { initialProps: { url: "url1", }, } );

그런 다음 첫 번째 렌더링이 isLoading 을 true로 반환하고 defaultValue 로 값을 반환하는지 테스트할 수 있습니다. 위에서 구현한 것과 정확히 유사합니다.

 expect(result.current[0]).toEqual(defaultValue); expect(result.current[1]).toBe(true);

비동기 업데이트를 테스트하기 위해 renderHook 이 반환한 wait 메서드를 사용할 수 있습니다. 그것은 act() 로 포장되어 있으므로 우리는 그것을 둘러싸고 act() 를 감쌀 필요가 없습니다.

 await wait(() => { expect(result.current[0].data).toEqual("url1"); }); expect(result.current[1]).toBe(false);

그런 다음 rerender 를 사용하여 새 소품으로 업데이트할 수 있습니다. 여기에 defaultValue 를 전달할 필요가 없습니다.

 rerender({ url: "url2" });

마지막으로 나머지 테스트도 비슷하게 진행됩니다(코드).

마무리

제 목표는 비동기 후크의 예를 들어 React Hooks를 테스트하는 방법을 보여주는 것이었습니다. 대부분의 후크에 동일한 접근 방식이 적용되어야 하므로 이것이 모든 종류의 후크 테스트를 자신 있게 처리하는 데 도움이 되기를 바랍니다.

React-hooks-testing-library가 완성되었기 때문에 사용하는 것이 좋으며 지금까지 큰 문제가 발생하지 않았습니다. 문제가 발생하는 경우 이 기사에서 설명하는 복잡한 테스트 후크를 사용하여 문제에 접근하는 방법을 알게 되었습니다.