Panduan Lengkap untuk Menguji React Hooks

Diterbitkan: 2022-03-11

Kait diperkenalkan di React 16.8 pada akhir 2018. Mereka adalah fungsi yang menghubungkan ke komponen fungsional dan memungkinkan kita untuk menggunakan fitur status dan komponen seperti componentDidUpdate , componentDidMount , dan banyak lagi. Ini tidak mungkin sebelumnya.

Selain itu, hook memungkinkan kita untuk menggunakan kembali komponen dan logika status di berbagai komponen. Ini sulit dilakukan sebelumnya. Oleh karena itu, hook telah menjadi pengubah permainan.

Pada artikel ini, kita akan mengeksplorasi cara menguji React Hooks. Kami akan memilih kait yang cukup rumit dan bekerja untuk mengujinya.

Kami berharap Anda adalah seorang pengembang React yang sudah terbiasa dengan React Hooks. Jika Anda ingin memoles pengetahuan Anda, Anda harus melihat tutorial kami, dan inilah tautan ke dokumentasi resmi.

Kait yang Akan Kami Gunakan untuk Pengujian

Untuk artikel ini, kita akan menggunakan sebuah hook yang saya tulis di artikel saya sebelumnya, Stale-sambil memvalidasi ulang Pengambilan Data dengan React Hooks. Kaitnya disebut useStaleRefresh . Jika Anda belum membaca artikelnya, jangan khawatir karena saya akan merangkum bagian itu di sini.

Ini adalah kait yang akan kami uji:

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

Seperti yang Anda lihat, useStaleRefresh adalah pengait yang membantu mengambil data dari URL sambil mengembalikan versi data yang di-cache, jika ada. Ini menggunakan penyimpanan dalam memori sederhana untuk menyimpan cache.

Itu juga mengembalikan nilai isLoading yang benar jika belum ada data atau cache yang tersedia. Klien dapat menggunakannya untuk menunjukkan indikator pemuatan. Nilai isLoading disetel ke false saat cache atau respons baru tersedia.

Bagan alur yang melacak logika basi-sambil menyegarkan

Pada titik ini, saya akan menyarankan Anda meluangkan waktu membaca hook di atas untuk mendapatkan pemahaman yang lengkap tentang apa fungsinya.

Pada artikel ini, kita akan melihat bagaimana kita dapat menguji hook ini, pertama menggunakan tanpa library pengujian (hanya React Test Utilities dan Jest) dan kemudian dengan menggunakan react-hooks-testing-library.

Motivasi di balik tidak menggunakan pustaka pengujian, yaitu, hanya pelari uji Jest , adalah untuk mendemonstrasikan cara kerja pengujian kait. Dengan pengetahuan itu, Anda akan dapat men-debug masalah apa pun yang mungkin muncul saat menggunakan perpustakaan yang menyediakan abstraksi pengujian.

Mendefinisikan Kasus Uji

Sebelum kita mulai menguji hook ini, mari kita buat rencana apa yang ingin kita uji. Karena kita tahu apa yang seharusnya dilakukan kail, inilah rencana delapan langkah saya untuk mengujinya:

  1. Ketika pengait dipasang dengan URL url1 , isLoading adalah true dan data adalah defaultValue .
  2. Setelah permintaan pengambilan asinkron, hook diperbarui dengan data data1 dan isLoading adalah false .
  3. Ketika URL diubah menjadi url2 , isLoading menjadi true lagi dan data menjadi defaultValue .
  4. Setelah permintaan pengambilan asinkron, kait diperbarui dengan data baru data2 .
  5. Kemudian, kami mengubah URL kembali ke url1 . Data data1 langsung diterima karena di-cache. isLoading salah.
  6. Setelah permintaan pengambilan asinkron, saat respons baru diterima, data diperbarui ke data3 .
  7. Kemudian, kami mengubah URL kembali ke url2 . Data data2 langsung diterima karena di-cache. isLoading salah.
  8. Setelah permintaan pengambilan asinkron, saat respons baru diterima, data diperbarui ke data4 .

Alur uji yang disebutkan di atas dengan jelas mendefinisikan lintasan bagaimana kait akan berfungsi. Karena itu, jika kami dapat memastikan tes ini berhasil, kami baik-baik saja.

Aliran uji

Menguji Kait Tanpa Perpustakaan

Di bagian ini, kita akan melihat cara menguji kait tanpa menggunakan pustaka apa pun. Ini akan memberi kita pemahaman mendalam tentang cara menguji React Hooks.

Untuk memulai pengujian ini, pertama-tama, kami ingin membuat mock fetch . Ini agar kami dapat memiliki kendali atas apa yang dikembalikan oleh API. Ini adalah fetch yang diolok-olok.

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

fetch yang dimodifikasi ini mengasumsikan bahwa tipe respons selalu JSON dan, secara default, mengembalikan url parameter sebagai nilai data . Itu juga menambahkan penundaan acak antara 200 ms dan 500 ms ke respons.

Jika kita ingin mengubah respon, kita cukup menyetel suffix argumen kedua ke nilai string yang tidak kosong.

Pada titik ini, Anda mungkin bertanya, mengapa penundaan? Mengapa kita tidak langsung membalasnya saja? Ini karena kami ingin meniru dunia nyata sebanyak mungkin. Kami tidak dapat menguji kail dengan benar jika kami mengembalikannya secara instan. Tentu, kami dapat mengurangi penundaan menjadi 50-100 ms untuk pengujian yang lebih cepat, tetapi jangan khawatir tentang itu di artikel ini.

Dengan mengambil mock kita siap, kita dapat mengaturnya ke fungsi fetch . Kami menggunakan beforeAll dan afterAll untuk melakukannya karena fungsi ini tidak memiliki status sehingga kami tidak perlu mengatur ulang setelah pengujian individual.

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

Kemudian, kita perlu memasang kait di sebuah komponen. Mengapa? Karena kait hanya berfungsi sendiri. Hanya ketika digunakan dalam komponen, mereka dapat merespons useState , useEffect , dll.

Jadi, kita perlu membuat TestComponent yang membantu kita memasang hook.

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

Ini adalah komponen sederhana yang merender data atau membuat prompt teks "Memuat" jika data sedang dimuat (sedang diambil).

Setelah kita memiliki komponen pengujian, kita perlu memasangnya di DOM. Kami menggunakan beforeEach dan afterEach untuk memasang dan melepas komponen kami untuk setiap pengujian karena kami ingin memulai dengan DOM baru sebelum setiap pengujian.

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

Perhatikan bahwa container harus menjadi variabel global karena kami ingin memiliki akses ke sana untuk pernyataan pengujian.

Dengan set itu, mari kita lakukan pengujian pertama di mana kita merender URL url1 , dan karena mengambil URL akan memakan waktu (lihat fetchMock ), itu harus merender teks "memuat" pada awalnya.

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

Jalankan tes menggunakan yarn test , dan itu berfungsi seperti yang diharapkan. Berikut kode lengkapnya di GitHub.

Sekarang, mari kita uji kapan teks loading ini berubah menjadi data respons yang diambil, url1 .

Bagaimana kita melakukannya? Jika Anda melihat fetchMock , Anda melihat kami menunggu selama 200-500 milidetik. Bagaimana jika kita menempatkan sleep dalam tes yang menunggu 500 milidetik? Ini akan mencakup semua kemungkinan waktu tunggu. Mari kita coba itu.

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

Tes lulus, tetapi kami juga melihat kesalahan (kode).

 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(...).

Ini karena pembaruan status di kait useStaleRefresh terjadi di luar act(). Untuk memastikan pembaruan DOM diproses tepat waktu, React menyarankan Anda menggunakan act() setiap kali rendering ulang atau pembaruan UI mungkin terjadi. Jadi, kita perlu membungkus tidur kita dengan act karena inilah saatnya pembaruan status terjadi. Setelah melakukannya, kesalahan akan hilang.

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

Sekarang, jalankan lagi (kode di GitHub). Seperti yang diharapkan, itu berlalu tanpa kesalahan.

Mari kita uji situasi selanjutnya di mana pertama-tama kita mengubah URL menjadi url2 , lalu periksa layar loading , lalu tunggu respons pengambilan, dan terakhir periksa teks url2 . Karena kita sekarang tahu cara menunggu perubahan asinkron dengan benar, ini seharusnya mudah.

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

Jalankan tes ini, dan itu lulus juga. Sekarang, kami juga dapat menguji kasus di mana data respons berubah dan cache ikut bermain.

Anda akan melihat bahwa kami memiliki suffix argumen tambahan dalam fungsi fetchMock kami. Ini untuk mengubah data respons. Jadi kami memperbarui mock pengambilan kami untuk menggunakan suffix .

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

Sekarang, kita dapat menguji kasus di mana URL disetel ke url1 lagi. Pertama-tama memuat url1 dan kemudian url1__ . Kita dapat melakukan hal yang sama untuk url2 , dan seharusnya tidak ada kejutan.

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

Seluruh tes ini memberi kita keyakinan bahwa hook memang bekerja seperti yang diharapkan (kode). Hore! Sekarang, mari kita lihat sekilas pengoptimalan menggunakan metode pembantu.

Mengoptimalkan Pengujian dengan Menggunakan Metode Helper

Sejauh ini, kita telah melihat cara menguji kail kita sepenuhnya. Pendekatannya tidak sempurna tetapi berhasil. Namun, bisakah kita melakukan yang lebih baik?

Ya. Perhatikan bahwa kami menunggu 500 md tetap untuk setiap pengambilan selesai, tetapi setiap permintaan membutuhkan waktu mulai dari 200 hingga 500 md. Jadi, kami jelas membuang-buang waktu di sini. Kami dapat menangani ini lebih baik dengan hanya menunggu waktu yang dibutuhkan setiap permintaan.

Bagaimana kita melakukannya? Teknik sederhana adalah mengeksekusi pernyataan sampai melewati atau batas waktu tercapai. Mari kita buat fungsi waitFor yang melakukan itu.

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

Fungsi ini hanya menjalankan panggilan balik (cb) di dalam blok try...catch setiap 10 md, dan jika timeout tercapai, itu akan menimbulkan kesalahan. Hal ini memungkinkan kita untuk menjalankan sebuah pernyataan sampai lulus dengan cara yang aman (yaitu, tidak ada loop tak terbatas).

Kami dapat menggunakannya dalam pengujian kami sebagai berikut: Alih-alih tidur selama 500 ms dan kemudian menegaskan, kami menggunakan fungsi waitFor kami.

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

Lakukan dalam semua pernyataan seperti itu, dan kami dapat melihat perbedaan yang cukup besar dalam seberapa cepat pengujian kami berjalan (kode).

Sekarang, semua ini bagus, tapi mungkin kita tidak ingin menguji hook melalui UI. Mungkin kita ingin menguji sebuah kail menggunakan nilai kembaliannya. Bagaimana kita melakukannya?

Itu tidak akan sulit karena kita sudah memiliki akses ke nilai pengembalian hook kita. Mereka hanya di dalam komponen. Jika kita dapat membawa variabel-variabel itu ke lingkup global, itu akan berhasil. Jadi mari kita lakukan itu.

Karena kita akan menguji hook kita melalui nilai kembaliannya dan bukan merender DOM, kita dapat menghapus render HTML dari komponen kita dan membuatnya menjadi null . Kita juga harus menghapus destructuring di hook's return untuk membuatnya lebih umum. Jadi, kami memiliki komponen pengujian yang diperbarui ini.

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

Sekarang nilai pengembalian hook disimpan di result , sebuah variabel global. Kami dapat menanyakannya untuk pernyataan kami.

 // 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");

Setelah kami mengubahnya di mana-mana, kami dapat melihat pengujian kami lulus (kode).

Pada titik ini, kita mendapatkan inti dari pengujian React Hooks. Ada beberapa perbaikan yang masih bisa kami lakukan, seperti:

  1. Memindahkan variabel result ke lingkup lokal
  2. Menghapus kebutuhan untuk membuat komponen untuk setiap kait yang ingin kami uji

Kita dapat melakukannya dengan membuat fungsi pabrik yang memiliki komponen uji di dalamnya. Itu juga harus membuat pengait di komponen uji dan memberi kita akses ke variabel result . Mari kita lihat bagaimana kita bisa melakukannya.

Pertama, kita pindahkan TestComponent dan result di dalam fungsi. Kita juga perlu meneruskan argumen Hook dan Hook sebagai argumen fungsi sehingga dapat digunakan dalam komponen pengujian kita. Menggunakan itu, inilah yang kita miliki. Kami memanggil fungsi ini renderHook .

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

Alasan kami memiliki result sebagai objek yang menyimpan data di result.current adalah karena kami ingin nilai yang dikembalikan diperbarui saat pengujian berjalan. Nilai kembalian dari hook kita adalah sebuah array, jadi itu akan disalin berdasarkan nilai jika kita mengembalikannya secara langsung. Dengan menyimpannya dalam sebuah objek, kita mengembalikan referensi ke objek tersebut sehingga nilai yang dikembalikan dapat diperbarui dengan memperbarui result.current .

Sekarang, bagaimana cara kita memperbarui hook? Karena kita sudah menggunakan penutupan, mari kita sertakan rerender fungsi lain yang dapat melakukannya.

Fungsi renderHook terakhir terlihat seperti ini:

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

Sekarang, kita dapat menggunakannya dalam pengujian kita. Alih-alih menggunakan act dan render , kami melakukan hal berikut:

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

Kemudian, kita dapat menegaskan menggunakan result.current dan memperbarui hook menggunakan rerender . Berikut ini contoh sederhana:

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

Setelah Anda mengubahnya di semua tempat, Anda akan melihatnya berfungsi tanpa masalah (kode).

Cemerlang! Sekarang kita memiliki abstraksi yang jauh lebih bersih untuk menguji hook. Kami masih bisa melakukan yang lebih baik - misalnya, defaultValue harus diteruskan setiap kali untuk rerender meskipun tidak berubah. Kami bisa memperbaikinya.

Tapi jangan bertele-tele karena kami sudah memiliki perpustakaan yang meningkatkan pengalaman ini secara signifikan.

Masukkan react-hooks-testing-library.

Pengujian Menggunakan React-hooks-testing-library

React-hooks-testing-library melakukan semua yang telah kita bicarakan sebelumnya dan kemudian beberapa. Misalnya, ini menangani pemasangan dan pelepasan wadah sehingga Anda tidak perlu melakukannya di file pengujian Anda. Ini memungkinkan kami untuk fokus pada pengujian hook kami tanpa terganggu.

Muncul dengan fungsi renderHook yang mengembalikan rerender dan result . Itu juga mengembalikan wait , yang mirip dengan waitFor , jadi Anda tidak perlu mengimplementasikannya sendiri.

Berikut adalah bagaimana kita membuat sebuah hook di React-hooks-testing-library. Perhatikan hook dilewatkan dalam bentuk callback. Callback ini dijalankan setiap kali komponen pengujian dirender ulang.

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

Kemudian, kita dapat menguji apakah render pertama menghasilkan isLoading sebagai true dan mengembalikan nilai sebagai defaultValue dengan melakukan ini. Persis seperti yang kami terapkan di atas.

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

Untuk menguji pembaruan async, kita dapat menggunakan metode wait yang renderHook . Itu datang dibungkus dengan act() jadi kita tidak perlu membungkus act() di sekitarnya.

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

Kemudian, kita dapat menggunakan rerender untuk memperbaruinya dengan alat peraga baru. Perhatikan bahwa kita tidak perlu melewatkan defaultValue di sini.

 rerender({ url: "url2" });

Akhirnya, sisa tes akan berjalan dengan cara yang sama (kode).

Membungkus

Tujuan saya adalah untuk menunjukkan kepada Anda bagaimana menguji React Hooks dengan mengambil contoh async hook. Saya harap ini membantu Anda dengan percaya diri menangani pengujian jenis kait apa pun, karena pendekatan yang sama harus diterapkan pada sebagian besar dari mereka.

Saya akan merekomendasikan Anda menggunakan React-hooks-testing-library karena sudah selesai, dan sejauh ini saya belum mengalami masalah yang berarti. Jika Anda mengalami masalah, Anda sekarang tahu cara mendekatinya menggunakan seluk-beluk kait pengujian yang dijelaskan dalam artikel ini.