Tepki Kancalarını Test Etmek İçin Eksiksiz Bir Kılavuz
Yayınlanan: 2022-03-11Kancalar, 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.
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:
- Kanca URL
url1
ile bağlandığında,isLoading
true
ve datadefaultValue
. - Eşzamansız bir getirme isteğinden sonra, kanca veri
data1
ile güncellenir veisLoading
false
olur. - URL url2 olarak değiştirildiğinde,
url2
tekrar true olur ve dataisLoading
defaultValue
. - Eşzamansız bir getirme isteğinden sonra, kanca yeni veri
data2
ile güncellenir. - Ardından, URL'yi tekrar
url1
olarak değiştiririz. Veri verileri1data1
alındığından anında alınır.isLoading
yanlış. - Eşzamansız bir getirme isteğinden sonra, yeni bir yanıt alındığında veriler
data3
olarak güncellenir. - Ardından, URL'yi tekrar
url2
olarak değiştiririz. Veridata2
, önbelleğe alındığından anında alınır.isLoading
yanlış. - 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.
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:
-
result
değişkenini yerel bir kapsama taşıma - 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.