Tepki Kancalarını Test Etmek İçin Eksiksiz Bir Kılavuz

Yayınlanan: 2022-03-11

Kancalar, 2018'in sonlarında React 16.8'de tanıtıldı. Bunlar, işlevsel bir bileşene bağlanan ve componentDidUpdate , componentDidMount ve daha fazlası gibi durum ve bileşen özelliklerini kullanmamıza izin veren işlevlerdir. Bu daha önce mümkün değildi.

Ayrıca, kancalar, bileşen ve durum mantığını farklı bileşenler arasında yeniden kullanmamıza izin verir. Bunu daha önce yapmak zordu. Bu nedenle, kancalar bir oyun değiştirici olmuştur.

Bu yazıda, React Hooks'un nasıl test edileceğini keşfedeceğiz. Yeterince karmaşık bir kanca seçeceğiz ve test etmeye çalışacağız.

React Hooks'a zaten aşina olan hevesli bir React geliştiricisi olmanızı bekliyoruz. Bilginizi tazelemek istiyorsanız, öğreticimize göz atmalısınız ve işte resmi belgelere bağlantı.

Test İçin Kullanacağımız Kanca

Bu makale için, bir önceki makalem olan Stale-while-revalidate Data Fetching with React Hooks'ta yazdığım bir kancayı kullanacağız. Kanca useStaleRefresh olarak adlandırılır. Makaleyi okumadıysanız endişelenmeyin, o kısmı burada özetleyeceğim.

Test edeceğimiz kanca bu:

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

Gördüğünüz gibi, useStaleRefresh , varsa, verilerin önbelleğe alınmış bir sürümünü döndürürken bir URL'den veri almaya yardımcı olan bir kancadır. Önbelleği tutmak için basit bir bellek içi depo kullanır.

Ayrıca, henüz kullanılabilir veri veya önbellek yoksa doğru olan bir isLoading değeri döndürür. İstemci bunu bir yükleme göstergesi göstermek için kullanabilir. Önbellek veya yeni yanıt mevcut olduğunda isLoading değeri false olarak ayarlanır.

Yenileme sırasında eskime mantığını izleyen bir akış şeması

Bu noktada, ne yaptığını tam olarak anlamak için yukarıdaki kancayı okumak için biraz zaman ayırmanızı önereceğim.

Bu yazıda, önce test kitaplıkları kullanmadan (sadece React Test Utilities ve Jest) ve ardından tepki-hook-testing-library kullanarak bu kancayı nasıl test edebileceğimizi göreceğiz.

Test kitaplıkları kullanmamanın, yani yalnızca bir test çalıştırıcı Jest kullanmanın ardındaki motivasyon, bir kancayı test etmenin nasıl çalıştığını göstermektir. Bu bilgiyle, test soyutlaması sağlayan bir kitaplık kullanırken ortaya çıkabilecek sorunları ayıklayabilirsiniz.

Test Durumlarını Tanımlama

Bu kancayı test etmeye başlamadan önce, test etmek istediğimiz şeyin bir planını bulalım. Kancanın ne yapması gerektiğini bildiğimize göre, onu test etmek için sekiz adımlık planım:

  1. Kanca URL url1 ile bağlandığında, isLoading true ve data defaultValue .
  2. Eşzamansız bir getirme isteğinden sonra, kanca veri data1 ile güncellenir ve isLoading false olur.
  3. URL url2 olarak değiştirildiğinde, url2 tekrar true olur ve data isLoading defaultValue .
  4. Eşzamansız bir getirme isteğinden sonra, kanca yeni veri data2 ile güncellenir.
  5. Ardından, URL'yi tekrar url1 olarak değiştiririz. Veri verileri1 data1 alındığından anında alınır. isLoading yanlış.
  6. Eşzamansız bir getirme isteğinden sonra, yeni bir yanıt alındığında veriler data3 olarak güncellenir.
  7. Ardından, URL'yi tekrar url2 olarak değiştiririz. Veri data2 , önbelleğe alındığından anında alınır. isLoading yanlış.
  8. Eşzamansız bir getirme isteğinden sonra, yeni bir yanıt alındığında veriler data4 olarak güncellenir.

Yukarıda bahsedilen test akışı, kancanın nasıl çalışacağının yörüngesini açıkça tanımlar. Bu nedenle, bu testin çalışmasını sağlayabilirsek, iyiyiz.

Test akışı

Kancaları Kitaplık Olmadan Test Etme

Bu bölümde, herhangi bir kitaplık kullanmadan kancaların nasıl test edileceğini göreceğiz. Bu bize React Hooks'un nasıl test edileceğine dair derinlemesine bir anlayış sağlayacaktır.

Bu teste başlamak için önce, fetch ile alay etmek istiyoruz. Bu, API'nin ne döndürdüğü üzerinde kontrol sahibi olabilmemiz içindir. İşte alay fetch .

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

Bu değiştirilmiş fetch , yanıt türünün her zaman JSON olduğunu varsayar ve varsayılan olarak, data değeri olarak parametre url döndürür. Ayrıca yanıta 200ms ile 500ms arasında rastgele bir gecikme ekler.

Yanıtı değiştirmek istiyorsak, ikinci argüman suffix boş olmayan bir dize değerine ayarlamamız yeterlidir.

Bu noktada, neden gecikmeli diye sorabilirsiniz. Neden yanıtı anında geri vermiyoruz? Bunun nedeni, gerçek dünyayı mümkün olduğunca çoğaltmak istememizdir. Anında geri verirsek kancayı doğru şekilde test edemeyiz. Elbette, daha hızlı testler için gecikmeyi 50-100 ms'ye düşürebiliriz, ancak bu makalede bunun için endişelenmeyelim.

Getirme mock'ımız hazır olduğunda, onu fetch işlevine ayarlayabiliriz. Bunu yapmak için beforeAll ve afterAll kullanıyoruz çünkü bu işlev durumsuzdur, dolayısıyla bireysel bir testten sonra onu sıfırlamamız gerekmez.

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

Ardından, kancayı bir bileşene monte etmemiz gerekiyor. Niye ya? Çünkü kancalar sadece kendi başlarına işlevlerdir. Yalnızca bileşenlerde kullanıldıklarında useState , useEffect vb. işlevlere yanıt verebilirler.

Bu nedenle, kancamızı monte etmemize yardımcı olacak bir TestComponent oluşturmamız gerekiyor.

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

Bu, veri yükleniyorsa (getiriliyorsa) verileri işleyen veya bir "Yükleniyor" metin istemi oluşturan basit bir bileşendir.

Test bileşenine sahip olduğumuzda, onu DOM'a monte etmemiz gerekiyor. Her testten önce yeni bir DOM ile başlamak istediğimizden, her test için bileşenimizi takmak ve çıkarmak için beforeEach ve afterEach kullanırız.

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

Test iddiaları için ona erişmek istediğimizden, container global bir değişken olması gerektiğine dikkat edin.

Bu setle, bir URL url1 oluşturduğumuz ilk testimizi yapalım ve URL'yi getirme biraz zaman alacağından (bkz. fetchMock ), başlangıçta metni “yükleme” yapmalıdır.

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

yarn test testi kullanarak testi çalıştırın ve beklendiği gibi çalışır. İşte GitHub'daki kodun tamamı.

Şimdi, bu loading metninin alınan yanıt verisi olan url1 ne zaman değiştiğini test edelim.

Bunu nasıl yaparız? fetchMock bakarsanız, 200-500 milisaniye beklediğimizi görürsünüz. 500 milisaniye bekleyen teste bir sleep koyarsak ne olur? Tüm olası bekleme sürelerini kapsayacaktır. Bunu deneyelim.

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

Test geçer, ancak bir hata da görürüz (kod).

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

Bunun nedeni, useStaleRefresh kancasındaki durum güncellemesinin act() dışında gerçekleşmesidir. DOM güncellemelerinin zamanında işlendiğinden emin olmak için React, her yeniden oluşturma veya UI güncellemesi olduğunda act() kullanmanızı önerir. Bu nedenle, durum güncellemesinin gerçekleştiği zaman olduğu için uykumuzu act ile sarmamız gerekiyor. Bunu yaptıktan sonra, hata gider.

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

Şimdi tekrar çalıştırın (GitHub'daki kod). Beklendiği gibi hatasız geçer.

Önce URL'yi url2 olarak değiştirdiğimiz sonraki durumu test edelim, ardından loading ekranını kontrol edelim, ardından getirme yanıtını bekleyelim ve son olarak url2 metnini kontrol edelim. Artık zaman uyumsuz değişiklikleri nasıl doğru bir şekilde bekleyeceğimizi bildiğimiz için, bu kolay olmalı.

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

Bu testi çalıştırın ve o da geçer. Artık yanıt verilerinin değiştiği ve önbelleğin devreye girdiği durumu da test edebiliriz.

fetchMock işlevimizde ek bir argüman suffix ekimiz olduğunu fark edeceksiniz. Bu, yanıt verilerini değiştirmek içindir. Bu yüzden suffix kullanmak için getirme modelimizi güncelliyoruz.

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

Şimdi, URL'nin url1 olarak ayarlandığı durumu tekrar test edebiliriz. Önce url1 , ardından url1__ yükler. url2 için de yapabiliriz ve sürpriz olmamalı.

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

Bu testin tamamı bize kancanın gerçekten beklendiği gibi çalıştığına dair güven verir (kod). Yaşasın! Şimdi yardımcı yöntemleri kullanarak optimizasyona hızlıca bir göz atalım.

Yardımcı Yöntemleri Kullanarak Testi Optimize Etme

Şimdiye kadar, kancamızı tamamen nasıl test edeceğimizi gördük. Yaklaşım mükemmel değil ama işe yarıyor. Yine de daha iyisini yapabilir miyiz?

Evet. Her bir getirmenin tamamlanması için sabit bir 500 ms beklediğimize dikkat edin, ancak her istek 200 ila 500 ms arasında bir şey alır. Yani burada açıkça zaman kaybediyoruz. Her isteğin alacağı zamanı bekleyerek bunu daha iyi halledebiliriz.

Bunu nasıl yaparız? Basit bir teknik, iddiayı geçene veya bir zaman aşımına ulaşılana kadar yürütmektir. Bunu yapan bir waitFor işlevi oluşturalım.

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

Bu işlev, her 10 ms'de bir try...catch bloğu içinde bir geri arama (cb) çalıştırır ve timeout ulaşılırsa bir hata verir. Bu, güvenli bir şekilde geçene kadar (yani sonsuz döngü yok) bir iddia yürütmemize izin verir.

Bunu testimizde şu şekilde kullanabiliriz: 500ms uyuyup ardından ileri sürmek yerine waitFor fonksiyonumuzu kullanıyoruz.

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

Bunu tüm bu tür iddialarda yapın ve testimizin (kod) ne kadar hızlı çalıştığı konusunda önemli bir fark görebiliriz.

Şimdi, tüm bunlar harika, ama belki de kancayı UI aracılığıyla test etmek istemiyoruz. Belki bir kancayı dönüş değerlerini kullanarak test etmek istiyoruz. Bunu nasıl yaparız?

Zor olmayacak çünkü kancamızın dönüş değerlerine zaten erişimimiz var. Onlar sadece bileşenin içindeler. Bu değişkenleri global kapsama alabilirsek, işe yarayacaktır. Öyleyse yapalım.

Kancamızı dönüş değeriyle test edeceğimiz ve DOM oluşturulmadığı için, HTML oluşturmayı bileşenimizden kaldırabilir ve null oluşturmasını sağlayabiliriz. Daha genel hale getirmek için kancanın dönüşündeki tahribatı da kaldırmalıyız. Böylece, bu güncellenmiş test bileşenine sahibiz.

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

Artık kancanın dönüş değeri, global bir değişken olan result olarak depolanır. İddialarımız için sorgulayabiliriz.

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

Her yerde değiştirdikten sonra testlerimizin geçtiğini görebiliriz (kod).

Bu noktada, React Hooks'u test etmenin özünü alıyoruz. Hâlâ yapabileceğimiz birkaç iyileştirme var, örneğin:

  1. result değişkenini yerel bir kapsama taşıma
  2. Test etmek istediğimiz her kanca için bir bileşen oluşturma ihtiyacını ortadan kaldırmak

İçinde test bileşeni olan bir fabrika işlevi oluşturarak bunu yapabiliriz. Ayrıca test bileşenindeki kancayı oluşturmalı ve result değişkenine erişmemizi sağlamalıdır. Bunu nasıl yapabileceğimize bir bakalım.

İlk olarak, TestComponent hareket ettiriyoruz ve result fonksiyonun içinde buluyoruz. Ayrıca, test bileşenimizde kullanılabilmesi için Hook ve Hook argümanlarını fonksiyonun argümanları olarak iletmemiz gerekecek. Bunu kullanarak, elimizde ne var. Bu işlevi renderHook olarak adlandırıyoruz.

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

result , result.current içinde veri depolayan bir nesne olarak elde etmemizin nedeni, test çalışırken dönüş değerlerinin güncellenmesini istememizdir. Kancamızın dönüş değeri bir dizidir, bu nedenle doğrudan döndürseydik, değere göre kopyalanırdı. Bunu bir nesnede saklayarak, bu nesneye bir referans döndürürüz, böylece dönüş değerleri, result.current güncellenerek güncellenebilir.

Şimdi, kancayı nasıl güncelleyeceğiz? Halihazırda bir kapatma kullandığımız için, bunu yapabilen başka bir işlev yeniden oluşturmayı rerender .

Son renderHook işlevi şöyle görünür:

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

Artık testimizde kullanabiliriz. act ve render kullanmak yerine aşağıdakileri yaparız:

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

Ardından, rerender kullanarak iddiada bulunabilir ve result.current kullanarak kancayı güncelleyebiliriz. İşte basit bir örnek:

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

Her yerde değiştirdiğinizde sorunsuz (kod) çalıştığını göreceksiniz.

Parlak! Artık kancaları test etmek için çok daha temiz bir soyutlamamız var. Yine de daha iyisini yapabiliriz - örneğin, defaultValue değişmese bile rerender oluşturmak için her seferinde iletilmelidir. Biz bunu tamir edebiliriz.

Ancak bu deneyimi önemli ölçüde geliştiren bir kitaplığımız olduğu için çok fazla uğraşmayalım.

tepki-kanca-test-kütüphanesine girin.

React-hooks-test-kütüphanesini Kullanarak Test Etme

React-hooks-testing-library, daha önce bahsettiğimiz her şeyi ve sonra bazılarını yapar. Örneğin, kapsayıcı takma ve çıkarma işlemlerini gerçekleştirir, böylece bunu test dosyanızda yapmanız gerekmez. Bu, dikkatimiz dağılmadan kancalarımızı test etmeye odaklanmamızı sağlar.

Yeniden rerender ve result döndüren bir renderHook işleviyle birlikte gelir. Ayrıca waitFor benzer olan wait değerini döndürür, bu nedenle onu kendiniz uygulamanız gerekmez.

React-hooks-test-kütüphanesinde bir kancayı şu şekilde oluşturuyoruz. Kancanın bir geri arama şeklinde iletildiğine dikkat edin. Bu geri arama, test bileşeni her yeniden oluşturulduğunda çalıştırılır.

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

Ardından, bunu yaparak ilk oluşturmanın isLoading ile sonuçlanıp sonuçlanmadığını test edebilir ve değeri isLoading olarak defaultValue . Tam olarak yukarıda uyguladığımız şeye benzer.

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

Zaman uyumsuz güncellemeleri test etmek için renderHook döndürdüğü wait yöntemini kullanabiliriz. act() ile sarılmış olarak gelir, böylece act() 'ı etrafına sarmamıza gerek kalmaz.

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

Ardından, yeni aksesuarlarla güncellemek için rerender oluşturmayı kullanabiliriz. Burada defaultValue gerek olmadığına dikkat edin.

 rerender({ url: "url2" });

Son olarak, testin geri kalanı benzer şekilde ilerleyecektir (kod).

Toplama

Amacım, bir zaman uyumsuz kanca örneği alarak React Hooks'un nasıl test edileceğini göstermekti. Aynı yaklaşımın çoğu için geçerli olması gerektiğinden, umarım bu, her tür kancanın testini güvenle çözmenize yardımcı olur.

Tamamlandığından beri React-hooks-testing-library kullanmanızı tavsiye ederim ve şu ana kadar önemli bir sorunla karşılaşmadım. Bir sorunla karşılaşmanız durumunda, bu makalede açıklanan test kancalarının inceliklerini kullanarak soruna nasıl yaklaşacağınızı artık biliyorsunuz.