Frontend Testing — 3

Cumali Tezcan
Appcent
Published in
5 min readMar 24, 2024

Merhabalar, serimizin son yazısında test ile alakalı detay konulardan bahsedeceğiz. Başlamadan önce geçmişteki test yazılarımızı hatırlamak isteyenler için linklerimizi aşağıya bırakalım.
Frontend Testing : https://medium.com/appcent/frontend-testing-a6196f06d782
Frontend Testing 2 : https://medium.com/appcent/frontend-testing-2-f622808de11d

O halde hadi başlayalım …

İlk konumuz Custom Wrapper

Bilindiği üzere wrapper genellikle bir bileşenin veya işlevin çevresine sarılan veya etrafına yerleştirilen bir yapıdır. Bu yapı, genellikle ek işlevsellik eklemek veya mevcut işlevselliği değiştirmek için kullanılır.
Hadi wrapper kullanılan bir componenti test etme örneği gösterelim.

App.js

function App() {
return (
<div>
<p>Frontend Testing</p>
</div>
);
}

App.test.js

function WrapperComponent ({children})  {
return <div className='wrapper'>{children}</div>
}

test('should render the element correctly', async () => {
render(<App />, {
wrapper: WrapperComponent
});
screen.debug();
});

Ve ilk örneğimizin sonucu…

Kod yapısında da görüldüğü gibi “wrapper” class’ına sahip div başarıyla eklenmiş oldu.

Custom Renderer Setup

Custom Wrapper’dan sonra Custom Setup’dan bahsetmemiz iyi olacaktır. Önceki örneğimizde basit ve tek bir yapı göstermiştik fakat gerçek zamanlı projelerde uygulamanın çalışması için bizim de data akışında bulunduğumuz bir çok wrapper bulunabilir, bunların daha rahat yönetimi için tek tek import etmek yerine bir util page açmak daha doğru bir yaklaşımdır. Daha akılda kalması için hemen örneğimize geçelim.

App.js

function App() {
return (
<div>
<p>Frontend Testing</p>
</div>
);
}

App.test.js

import {customRender} from './test-utils';

function TestComponent () {
return <div>
<p>Frontend Testing</p>
</div>
}

test('should render the element correctly', async () => {
customRender(<TestComponent />);
screen.debug();
});

test-utils.js

function AuthProvider({children}){
return <div className="auth-provider">{children}</div>
}

function I18nProvider({children}) {
return <div className="i18n-provider">{children}</div>
}

function AllProviders ({children}) {
return(
<AuthProvider>
<I18nProvider>{children}</I18nProvider>
</AuthProvider>
)
}

export const customRender = (ui,options) => {
render(ui,{
wrapper:AllProviders,
...options
})
}

Ve sonuç.

Test işlemimiz başarılı.
Genel olarak büyük projelerde kullanılan yukarı seviye provider’ları her seferinde import etmek yerine örnekte de olduğu gibi bir test util yazarız ve default render function’u çalıştırmak yerine custom render’ları çalıştırırız. Örneğimizde de bu durumu göstermiş olduk.

Hook Testing ve Act()

Bu başlıkta custom hook’lardan ve utility function’lardan bahsedeceğiz.

Custom hook testi için renderHook’u import ederek örneğimize başlayalım.

App.test.js

import { renderHook} from '@testing-library/react';
import { useState } from 'react';

function useCustomHook() {
const [name,setName] = useState("Ahmet");

const changeName = (newName) => {
setName(newName);
}
return {name,changeName};
}

test('should render the element correctly', async () => {
const {result} = renderHook(useCustomHook);
expect(result.current.name).toBe("Ahmet");
});

Bu örnekte name ve setName alan ve default değeri Ahmet olan bir customHook oluşturduk, ardından changeName ile name değerini değiştiriyor ve geriye name ve changeName’i dönüyoruz. Bu şekilde customHook’umuz tamamlanmış oluyor.
Test kısmında ise renderHook’dan dönen result değerini destructuring ediyor ve kontrolümüzü sağlıyoruz.
Ve test sonucumuz.

Önceki kodumuzda yer alan changeName fonksiyonumuzu kullanalım.

import { useState } from 'react';

function useCustomHook() {
const [name,setName] = useState("Ahmet");

const changeName = (newName) => {
setName(newName);
}
return {name,changeName};
}

test('should render the element correctly', async () => {
const {result} = renderHook(useCustomHook);
expect(result.current.name).toBe("Ahmet");

result.current.changeName("Mehmet");
expect(result.current.name).toBe("Mehmet");
});

changeName sonrası test sonucumuz.

Bu sefer başarılı olmadı :)

Hata vermesinin sebebi changeName state’i değiştiriyor fakat biz bunu belirtmemiş oluyoruz. Yani hook’un state’in değiştiğini bilmesi ve update etmesi gerekiyor bunun içinde hata mesajında da yazdığı gibi act() yardımcı fonksiyonunu kullanmamız gerekiyor. Hadi act()’i uygulayalım.

import { useState } from 'react';
import { renderHook} from '@testing-library/react';

function useCustomHook() {
const [name,setName] = useState("Ahmet");

const changeName = (newName) => {
setName(newName);
}
return {name,changeName};
}

test('should render the element correctly', async () => {
const {result} = renderHook(useCustomHook);
expect(result.current.name).toBe("Ahmet");

act(()=>{
result.current.changeName("Mehmet");
})

expect(result.current.name).toBe("Mehmet");
});

act() fonksiyonunu ekledikten sonra test sonucumuz.

BeforeEach ve AfterEach

Son konumuza geçelim…

Burada temel mantık olarak projede tekrarlanan bir çok işlem yapıyoruz örneğin bir component’in render edilmesi bir database’e bağlanma işlemi gibi. Bu yüzden aynı şeyleri sürekli yazmış oluyoruz ve bunun çözümü için de karşımıza beforeEach() ve afterEach() çıkıyor.

İlk olarak beforeEach() ve afterEach() fonksiyonlarını çalışma mantığını kod ile gösterelim.

describe('Database Client', () => {
beforeEach(() => {
console.log("Here");
});

afterEach(() => {
console.log("After each");
});

test('should initialize with two users', () => {
console.log("test 1");
});

test('should delete a user', () => {
console.log("test 2");
});

test('should get a user', () => {
console.log("test 3");
});
});

Ve görüldüğü gibi lifecycle beforeEach(), test() ve afterEach() olarak çalışıyor.

Şimdi ise beforeEach() ve afterEach() kullanmadan bir Database Client işlemi yazalım ardından da fonksiyonlarımızı kullanarak yazalım ve aradaki farkı görmüş olalım.

beforeEach() ve afterEach() olmadan database işlemleri

class DatabaseClient{
constructor() {
this.users = [];
}

initialize() {
this.users = [
{id:1,name: "Ahmet"},
{id:2,name: "Mehmet"},
];
}

getUsers(){
return this.users;
}

getUser(id){
return this.users.find((user) => user.id === id);
}

deleteUser(id){
this.users = this.users.filter((user) => user.id !== id);
}

reset(){
this.users = [];
}
}

describe('Database Client', () => {
test('should initialize with two users', () => {
const dbClient = new DatabaseClient();
dbClient.initialize();

const users = dbClient.getUsers();

expect(users.length).toBe(2);
expect(users).toMatchObject([
{id:1, name:"Ahmet"},
{id:2, name:"Mehmet"},
]);
});

test('should delete a user', () => {
const dbClient = new DatabaseClient();
dbClient.initialize();

dbClient.deleteUser(1);

const user = dbClient.getUser(1);
expect(user).toBeFalsy();
});

test('should get a user', () => {
const dbClient = new DatabaseClient();
dbClient.initialize();

const user = dbClient.getUser(1);
expect(user).toMatchObject({id:1,name:"Ahmet"});
});
});

Testimiz başarılı fakat görüldüğü gibi 3 test fonksiyonumuzda da
const dbClient = new DatabaseClient();
dbClient.initialize();

kodumuzu kullanmak zorunda kaldık, daha fazla testimiz olsaydı orada da kullanacaktık. Çözüm için ise tekrar eden yapıları beforeEach() sayesinde çözeceğiz.

beforeEach() ve afterEach() kullanılınca

describe('Database Client', () => {
let dbClient;

beforeEach(() => {
dbClient = new DatabaseClient();
dbClient.initialize();
});


test('should initialize with two users', () => {
const users = dbClient.getUsers();

expect(users.length).toBe(2);
expect(users).toMatchObject([
{ id: 1, name: "Ahmet" },
{ id: 2, name: "Mehmet" },
]);
});

test('should delete a user', () => {
dbClient.deleteUser(1);

const user = dbClient.getUser(1);
expect(user).toBeFalsy();
});

test('should get a user', () => {
const user = dbClient.getUser(1);
expect(user).toMatchObject({ id: 1, name: "Ahmet" });
});
});

Evet, kod ekranında da görüldüğü üzere daha önce her test fonksiyonunda çalıştırdığımız dbClient’ın instance oluşturma ve onu initialize etme işlemini sadece beforeEach() içerisinde çalıştırarak gereksiz kod yükünden kurtulmuş olduk. Ek olarak afterEach() kullanarak da test sonrası işlemler yapabilirdik. Senaryomuza uygun kod yapımız ile bu şekilde göstermiş olduk. Ve son test sonucumuz.

Bu yazımız da bu kadar. Bugün detay konuları ele aldık.
Okuduğunuz için teşekkürler.

--

--