Angular sevdiğimiz bir abimizdi. Karmaşık bir framework'tü, her şeyi yapmaya
çalışırdı. Mesela ben yapmam. Bundle size'ı arttırırdı, ben arttırmam.
Developerlar'ın işi düştüğünde sırtını dönerdi, ben dönmem. Angular
sevdiğimiz bir abimizdi ama komponentleri de bir tuhaf render'lardı. Bizde
hook'lar masaya konur. Herkes ihtiyacı kadarını alır. Eskiler sende kalsın
kardeş, junior'ların kafasını karıştırma yeter. - React
Mocklama serüvenimizde React'a kadar geldik. Çoğumuz React framework'ünü kullanıyoruz ve komponent testleriyle
haşır neşir oluyoruz. Peki mocklama React komponentleriyle birlikte nasıl çalışır?
Yazının kodlarına Github üzerinden erişebilirsiniz.
Girizgah
Framework'lerle birlikte komponent dediğimiz özel fonksiyon türleri hayatımıza girer ve temel amaçları DOM elementri üretmek ve DOM ağacına render etmektir. Kullanıcının kayıtlı olup olmamasına göre formun içeriğini yöneten
Registration komponentini ele alalım.
Gördüğümüz üzere komponentler iç içe kullanılırlar ve uygulamanın giriş dosyasında özel bir metodla DOM ağacına render edilirler.
_10
import React from "react";
_10
import ReactDOM from "react-dom/client";
_10
import App from "./App";
_10
_10
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
_10
root.render(
_10
<React.StrictMode>
_10
<App />
_10
</React.StrictMode>
_10
);
İlgili dosya browser'da çalıştırıldığında DOM ağacı erişebilir haldedir. Ancak
testlerimiz NodeJS ortamında çalıştığından ötürü ortada DOM ağacı yoktur.
Komponentlerin test edilebilmesi için özel test kütüphaneleri
tarafından sahte DOM ağacı oluşturulur. Makalede Jest ile birlikte
React'tın en popüler test kütüphanesi olan React Testing Library
kullanacağız.
Komponentleri mocklamak için önceki yazılardan faydalanabilir ve döndüreceği DOM içeriğini belirleyebiliriz. Genelde div döndürüp data-testid parametresi olarak komponent ismi vermeyi tercih ediyorum.
render metodu DOM ağacıyla yapabileceğimiz işlemler için metodlar barındıran
bir nesne döndürür. Mock içeriklerin başarıyla render olduğunu görmek için debug metodunu kullanabiliriz. Çıktıya baktığımızda istediğimiz değerin
render edilmiş olduğunu görebiliriz. Komponentin başarıyla mocklandığını
görmenin huzuruyla yolumuza devam edelim.
Komponent Mocklama
Hatırlarsanız test yazarkenki temel prensibimiz, testin odaklandığı fonksiyon
dışındaki fonksiyonların mümkün mertebe çalıştırılmamasıydı. React'da ise child
komponentlerin render edilmemesini ve unit test gerekliliklerinin
karşılanmasını istiyoruz.
Test yazarken kendimize sormamız gereken soru "Bu komponentin görevi nedir?"
olmalıdır. Registration komponentinin görevi, isRegistered props değerine
göre hangi formun ekranda gösterileceğine karar vermektir. İçerisinde hangi
inputlarin olduğunu ve işlem bitince yönlendirdiği sayfayı test etmemeliyiz.
Bunlar formu render eden komponentin testinde ele alınmalıdır. data-testid
değeriyle doğru formun render edilip edilmediğini kontrol edebiliriz.
Örnekleri basit tutmaya çalışacağım ve her seferinde farklı biçimde yazışlar
göstereceğim ki kullanım örneklerini kafamızda çeşitlendirelim. Temel fikre
odaklanırsanız kendi örneklerinize uygulayabileceğinizi düşünüyorum.
Testlerimizi yazalım.
jest.mock
jest.spyOn
_38
import { render, screen } from "@testing-library/react";
FAIL src/TodoList.test.tsx <TodoList> tests ✕ should render only incomplete todo items (83 ms) Expected length: 2 Received length: 4
Mock başarılı... ama bir problem var gibi. Tamamlanan maddeler de gösteriliyor
ve hangi maddelerin render ettirildiğini belirten bir şey yok. Komponenti
mockladığımızda render olmasını engelleyen koşul da kaybolur. Tamamlanma
durumuna göre mock fonksiyonunun döndürdüğü değeri manipüle edebiliriz.
TodoList.test.tsx
_41
import { render, screen } from "@testing-library/react";
_41
import { TodoItemProps } from "./TodoItem";
_41
import TodoList from "./TodoList";
_41
_41
jest.mock("./TodoItem", () =>
_41
jest.fn(({ isCompleted, title }: TodoItemProps) => {
Bazen komponent state değerlerini alt komponentlerden değiştirip yeniden render
olmasını isteriz. Butona tıklandığında kişi listesinin çekildiği ve üst
komponentte render edildiği bir senaryo olabilir.
PersonList.tsx
Person.tsx
FetchButton.tsx
_23
import React, { useState } from "react";
_23
import FetchButton from "./FetchButton";
_23
import Person from "./Person";
_23
_23
const PersonList: React.FC = () => {
_23
const [persons, setPersons] = useState<any>([]);
_23
_23
return (
_23
<div>
_23
<FetchButton setPersons={setPersons} />
_23
{persons.map((person: any) => (
_23
<Person
_23
key={person.email}
_23
firstName={person.firstName}
_23
lastName={person.lastName}
_23
email={person.email}
_23
/>
_23
))}
_23
</div>
_23
);
_23
};
_23
_23
export default PersonList;
Bu senaryoyu FetchButton komponentine ilettiğimiz setPersons setter metoduyla örneklendirebiliriz. Bunun için div elementine onClick olayı tanımlayıp dönüş değerini özelleştireceğiz.
PersonList.test.tsx
_54
import { fireEvent, render, screen } from "@testing-library/react";
_54
import { PersonProps } from "./Person";
_54
import * as FetchButtonModule from "./FetchButton";
_54
import PersonList from "./PersonList";
_54
_54
// div içeriğinin gerçekte nasıl olduğu hiç önemli değil.
_54
// doğru kişiyi render ettiğimizi teyit edebiliyorsak kafi.
PASS src/PersonList.test.tsx <PersonList /> tests ✓ should get persons when click fetch button (36 ms)
PersonList komponentinin görevi state değerine göre kişileri render
ettirmektir. Dolayısıyla alt komponentin listeyi nereden ve nasıl çektiğiyle
ilgilenmez.
Kütüphane Komponentlerini Mocklama
Sayfalarımızın içeriği karmaşıklaştıkça yüklenme süresi şaha kalkar. Bu gibi durumlarda bildiğimiz üzere lazy load namı diğer tembel yükleme kullanırız. Adeta ödevini son güne bırakan tembel bir öğrenci misali komponentin ekranda gözükmesi yaklaşana kadar render etmez. Testleri yaptığımız NodeJS ortamında ekran dediğimiz kavram da olmadığı için komponentler hiçbir zaman render olmaz ve testleri gerçekleştiremeyiz.
Örneğimizi rastgele seçtiğim react-lazyload kütüphanesi üzerinden
gösteriyor olacağım. Bunun yerine diğer herhangi bir kütüphane gelebilir.
FAIL src/Photos.test.tsx <Photos /> tests ✕ should render greeting text without name (1083 ms) wUnable to find an element by: [data-testid="photo"]
img elementi görmek yerine yer tutucuyla karşılaşıyoruz. NodeJS'de
intersection observer olmadığından ötürü hiçbir zaman resim elementi DOM'a
render olmaz. Bu şekilde testimizi gerçekleştiremeyeceğimizden dolayı
LazyLoad komponentini mocklayacağız.
Photos.test.tsx
_61
import { render, screen } from "@testing-library/react";
_61
import axios from "axios";
_61
import * as ReactLazyLoadModule from "react-lazyload";
_61
import Photos from "./Photos";
_61
_61
describe("<Photos /> tests", () => {
_61
let mockPhotos: Array<{
_61
id: number,
_61
title: string,
_61
url: string,
_61
}>;
_61
_61
beforeAll(() => {
_61
mockPhotos = [
_61
{
_61
id: 1,
_61
title: "accusamus beatae ad facilis cum similique qui sunt",
_61
url: "https://via.placeholder.com/600/92c952",
_61
},
_61
{
_61
id: 2,
_61
title: "reprehenderit est deserunt velit ipsam",
_61
url: "https://via.placeholder.com/600/771796",
_61
},
_61
{
_61
id: 3,
_61
title: "officia porro iure quia iusto qui ipsa ut modi",
_61
url: "https://via.placeholder.com/600/24f355",
_61
},
_61
];
_61
_61
jest.spyOn(axios, "get").mockImplementation((url: string): any => {
_61
if (url === "https://jsonplaceholder.typicode.com/photos") {
_61
return Promise.resolve(mockPhotos);
_61
}
_61
_61
return Promise.reject();
_61
});
_61
});
_61
_61
test("should render greeting text without name", async () => {
_61
jest
_61
.spyOn(ReactLazyLoadModule, "default")
_61
.mockImplementation(({ children }: any) => children);
_61
_61
const { debug } = render(<Photos />);
_61
_61
// useEffect'teki fetch metodu ile veriyi çektikten sonra
_61
// state değerini güncelliyoruz. ancak bu durum meydana
_61
// gelmesini beklediğimiz render test içerisinde beklenmez.
_61
// ve eski dom içeriği görürüz. testi bekletmek adına `photo`
_61
// test id'li elementin render edilmesini bekleriz.
<body> <div> <div> <img alt="accusamus beatae ad facilis cum similique qui sunt" data-testid="photo" src="https://via.placeholder.com/600/92c952" /> <img alt="reprehenderit est deserunt velit ipsam" data-testid="photo" src="https://via.placeholder.com/600/771796" /> <img alt="officia porro iure quia iusto qui ipsa ut modi" data-testid="photo" src="https://via.placeholder.com/600/24f355" /> </div> </div></body>
PASS src/components/lazyload/Photos.test.tsx <Photos /> tests ✓ should render greeting text without name (55 ms)
Seri Sonu Canavarı
Yazının sonuna gelirken serideki bütün konuları birleştiren ve size challange
olacak bir örnek koymak istedim. Geriye doğru sayma vazifesi icra eden mini
React projesinin kodlarını aşağıya bıraktım. Buyrun testlerini yazmaya. Testin
çıktısından faydalanabilirsiniz.
RemainingTimer.tsx
TimeBox.tsx
utils/date.ts
_52
import React, { useEffect, useState } from "react";
_52
import TimeBox from "./TimeBox";
_52
import { calculateDeltaBetweenDates, convertDeltaToDaysHoursMinutes } from "./utils/date";
PASS src/RemainingTimer.test.tsx <RemainingTimer /> tests ✓ should render time that returned convertDeltaToDaysHoursMinutes when delta greater than zero (79 ms) ✓ should render timer ended text when delta equal zero (41 ms)
Kapanış
React komponentlerine değindiğimiz bu yazıyla birlikte mocking serim sona erdi.
Umarım açıklayıcı olmuştur. Testle ilgili İngilizce olarak bile çok büyük bir
kaynak sıkıntısı varken bu yazının can suyu olmasını umuyorum. Sonuna kadar
okuduysanız birkaç projenin testine giriştikten sonra yola alfa testçi olarak
devam edebileceğinizi düşünüyorum. Seri hakkında geribildirimlerinizi duymaktan
mutlu ve motive olurum.