Jest ile JavaScript kodu nasıl test edilir?

Zafer Ayan
Fiba Tech Lab
Published in
7 min readJul 2, 2021
Jest Nedir? — Logo: seeklogo.com

Özellikle React projelerinde karşılaştığımız Jest, Facebook firmasının geliştirmiş olduğu bir Javascript test kütüphanesidir. React kodunu test etmek için biçilmiş bir kaftan olmasının yanında herhangi bir JavaScript kodu için de kullanılabilir. Bu yazıda Jest ile ilgili temel bilgilere değineceğim. İsterseniz Jest’in projeye kurulumuna başlayalım.

Kurulum

Mevcut React projelerinde dahili olarak gelen Jest’i sıfırdan yüklemek için öncelikle boş bir proje oluşturalım:

yarn init -y 

Bu komut ile birlikte proje içerisine package.jsondosyası oluşacaktır. Şimdi Jest’i aşağıdaki gibi yükleyelim:

yarn add -D jest

- D komutu sayesinde Jest, sadece geliştirim ortamına eklenmiş olacak ve proje bundle’ına dahil edilmeyecektir.

jest’i yarn test ile çalıştırabilmek için scripts içerisine aşağıdaki gibi ekleyelim:

{
"scripts": {
"test": "jest"
}
}

Şimdi test edilebilecek basit bir kod ekleyelim. index.js dosyası oluşturup içerisine aşağıdaki gibi bir toplama fonksiyonu ekleyelim:

const topla = (a, b) => a + b;module.exports = { topla };

Şimdi kodu test edebilmek için index.test.js dosyasını oluşturalım:

const { topla } = require('./index')

test('2 + 2 = 4 eder', () => {
expect(topla(2, 2)).toBe(4);
}

Kodu açıklayacak olursak:

  • test: Testin tanımlanmasını sağlar ve içine aldığı parametre ile birlikte açıklayıcı bir metin girilmesi beklenir. ‘2 + 2 = 4 eder’ ifadesi de ilgili testi nitelemektedir.
  • expect: Test edilecek ifadeyi parametre olarak alır. Biz metodu çalıştırıp sonucunu expect’e vermiş olduk
  • toBe: expect’ten dönen değer ile karşılaştırılacak değeri belirlenmesi içindir. Matcher olarak da ifade edilir. toEqual, toBeNull, toBeDefined gibi pek çok farklı matcher bulunur.

Şimdi kodu yarn test komutu ile çalıştırdığınızda aşağıdaki gibi bir sonuç alacaksınız:

Geliştirim boyunca sürekli kodun jest tarafından test edilmesini istiyorsanız aşağıdaki gibi kullanabilirsiniz.

yarn jest --watchAll

Komutu çalıştırdığınızda, Jest terminalde sürekli proje içerisindeki değişiklikleri takip edecek ve buna göre testleri yeniden çalıştıracaktır. Kısacası herhangi bir dosyayı save ettiğinizde jest otomatik olarak tetiklenecektir. Test etmek için kodu aşağıdaki gibi bozalım:

const topla = (a, b) => a + b + 2;
module.exports = { topla };

Kodu kaydettiğinizde Jest’in çalıştığı terminalde aşağıdaki gibi hata verecektir:

Gördüğünüz gibi testte 4 çıktısını beklemişiz fakat koddan gelen 6 değerini görüyoruz. Bu şekilde testlerinizi anında çalıştırabilirsiniz.

Setup ve teardown fonksiyonları

Testlerin öncesinde veritabanının açılması ve sonrasında da kapatılması gibi işlemleri gerçekleştirmek için setup/before() ve teardown/after() fonksiyonlarından yararlanabiliriz.

beforeAll(() => {
return cityVeritabaniniAc();
});
afterAll(() => {
return cityVeritabaniniKapat();
});

test('City veritabanında İstanbul vardır.', () => {
expect(sehirVarMi('İstanbul')).toBeTruthy();
});
test('City veritabanında İzmir vardır.', () => {
expect(sehirVarMi('İzmir')).toBeTruthy();
});

describe() ile testlerin gruplandırılması

Sadece “city” veritabanı değil, “food” gibi diğer veritabanları üzerinde de testler yazmak isteyebiliriz. Bu amaçla ilişkili testleri describe ile gruplayarak before ve after fonksiyonlarını sadece o grup/scope içerisinde çalışacak hale getirebilirsiniz:

beforeEach(() => {
return cityVeritabaniniAc();
});
test('City veritabanında İstanbul vardır.', () => {
expect(sehirVarMi('İstanbul')).toBeTruthy();
});
test('City veritabanında İzmir vardır.', () => {
expect(sehirVarMi('İzmir')).toBeTruthy();
});
describe('Şehirlerin meşhur yiyecekleri', () => {
// Sadece bu describe bloğundaki testler için çalışır
beforeEach(() => {
return foodVeritabanınıAç();
});
test('İstanbul <3 balık ekmek', () => {
expect(isValidCityFoodPair('İstanbul', 'Balık ekmek')).toBe(true);
});
test('İzmir <3 boyoz', () => {
expect(isValidCityFoodPair('İzmir', 'Boyoz')).toBe(true);
});
});

Asenkron kodların test edilmesi

JavaScript’te callback ve promise’ler kullanılarak asenkron işlemler gerçekleşmektedir. Şimdi bu iki kapsamı ayrı ayrı değerlendirelim.

Callback kullanarak

Callback’li yapılar, kendisinden sonra gelen kodların direkt olarak çalıştırılarak devam ettirilmesine izin vermektedir. Jest’te callback kullanıldığında, test dosyası direkt olarak çalıştırılıp, callback’in çalışmasına zaman kalmadığı için yanlış çıktı üretebilir. Bunu engellemek için, Jest’teki callback bitimini bekleyen done() fonksiyonu kullanılabilir.

Şimdi aşağıdaki gibi uzak sunucudan veri getiren bir metodumuz olduğunu düşünelim. (Burada, kodu basit hale getirmek için 3 saniye timeout’lu bir fonksiyon ekledim.) Bu fonksiyon çalıştıktan 3 saniye sonra parametre olarak gelen callback metodunu ‘Hello world’ parametresi geçirerek çalıştıracak. Bu kodu getData.js dosyası içerisine kaydedelim:

function getData(callback) {
setTimeout(() => {
callback('Hello world');
}, 3000);
}
module.exports = getData;

Şimdi bu fonksiyonun testini, her zamanki senkron testler gibi yazmaya çalışalım. Ama testin patlaması için ‘asdasd’ gibi bir beklentimiz olsun. getData.test.js dosyası içerisine kaydedelim ve jest’i çalıştıralım:

const getData = require('./getData')test(`'Hello world' datasını getirir.`, () => {
function callback(data) {
expect(data).toBe('asdasdasd');
}
getData(callback);
});

Gördüğünüz gibi metot asenkron olarak çalıştığı için yazdığımız kod “testi hem geçti, hem de geçemedi” gibi garip bir durum oluşuyor. Bunu önlemek için test metodunun ikinci parametresi olan () => {fonksiyonuna parametre olarak done verelim ve expectmetodundan sonra çalışacak şekilde aşağıdaki gibi kullanalım:

const getData = require('./getData')test(`'Hello world' datasını getirir.`, (done) => {
function callback(data) {
expect(data).toBe('asdasdasd');
done();
}
getData(callback);
});

Gördüğünüz gibi done fonksiyonu kullanıldığında Jest, doneile aynı scope içerisinde bulunan ifadelerin çalışmasını bekleyerek, işler bittikten sonra testin sonucunu ekrana doğru bir şekilde bastırmaktadır.

‘Hello world’ çıktısını bekleyecek şekilde ayarladığınızda da yine doğru biçimde çalıştığını göreceksiniz:

Promise kullanarak

getData.js dosyasını aşağıdaki gibi promise dönecek şekilde değiştirelim:

function getData(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello world');
}, 3000);
})
}
module.exports = getData;

getData.test.js dosyasında kullanımı da aşağıdaki gibi olmalıdır:

const getData = require('./getData');test(`'Hello world' datasını getirir.`, () => {
return getData().then(data => {
expect(data).toBe('Hello world');
});
});

Buradaki return ifadesi önemli. Çünkü return ifadesini kullanmadan yazdığınızda, callback’li yapıdaki gibi yanlış bir çalışma şekli oluşmaktadır.

.resolves() rejects() kullanımı

Üstteki gibi uzun uzadıya yazmak yerine resolves matcher’ı aşağıdaki gibi kullanılabilir:

test(`'Hello world' datasını getirir.`, () => {
return expect(getData()).resolves.toBe('Hello world');
});

Benzer şekilde rejects kullanımı da aşağıdaki gibidir:

test(`'Hello world' datasını getirirken hata oluşur.`, () => {
return expect(getData()).rejects.toMatch('error');
});

async/await’li kullanım için de jest’in dokümanına bakabilirsiniz.

Mock fonksiyonları

Uygulama içerisinde veritabanı, network erişimi, dosya erişimi vb. dışa bağımlı kısımlar bulunabilir. Bu kısımlar test edildiğinde, testlerin çalışma sürelerini uzatacakları için, bu kısımların yalancı (mock) versiyonları oluşturulur. Bu sayede yüzlerce testiniz olsa dahi çok hızlı bir biçimde çalıştırabilirsiniz. Ayrıca veritabanı ve dosyaya yazıp silme gibi yan etkili işler yapmanıza da gerek kalmaz.

Şimdi myjson sitesinden profile json metnini axios ile getirecek fonksiyonu yazalım. axios’u yüklemek için her zamanki gibi yarn add axios komutuyla yükleyebilirsiniz. getProfile.js dosyası aşağıdaki gibi kodlayalım:

const axios = require('axios');async function getProfile() {
const url =
'https://my-json-server.typicode.com/typicode/demo/profile';

try {
return await axios.get(url);
} catch (error) {
throw error;
}
}
module.exports = getProfile;

Şimdi axios isteklerini mocklamak için getProfile.test.js dosyasını aşağıdaki gibi oluşturalım:

const axios = require('axios');
const getProfile = require('./getProfile');
jest.mock("axios");const mockData = {
"name": "typicode"
}

axios.get.mockImplementation(() => Promise.resolve(mockData));
test("getProfile typicode profil bilgisini getirmelidir.", () => {
return expect(getProfile()).resolves.toStrictEqual(
{
"name": "typicode"
}
);
});

Burada da gördüğünüz gibi axios’un get isteğini mocklayarak istediğimiz datayı dönmesini sağladık. Üstelik bu işlemi herhangi bir network çağrısı kullanmadan yaptığımız için çok kısa bir sürede de testleri çalıştırmış olduk.

Snapshot testleri

Snapshot testleri aslında ekranın bir görüntüsünü alıp kaydederek daha sonra kıyaslamak için kullanılmasıdır desek çok da yanlış olmaz. Çünkü snapshot testlerinde UI bileşeni render edilir ve bir dosya içerisine kaydedilir. Test sonraki çalıştırmada kaydedilmiş dosyanın içeriğini okur ve yeni render ettiği UI bileşeni ile karşılaştırır.

Bunu görebilmek için aşağıdaki gibi bir react projesi oluşturalım:

npx create-react-app test-jest-react

Ayrıca UI bileşenini render edip JSON’a dönüştürecek olan react-test-renderer kütüphanesini de ekleyelim:

yarn add -D react-test-renderer

Şimdi projeye linkleri görüntüleyecek olan bir <Link> bileşeni ekleyelim. Dosyanın adını Link.react.js olarak verebiliriz:

import React, {useState} from 'react';const STATUS = {
HOVERED: 'hovered',
NORMAL: 'normal',
};
const Link = ({page, children}) => {
const [status, setStatus] = useState(STATUS.NORMAL);
const onMouseEnter = () => {
setStatus(STATUS.HOVERED);
};
const onMouseLeave = () => {
setStatus(STATUS.NORMAL);
};
return (
<a
className={status}
href={page || '#'}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</a>
);
};
export default Link;

Şimdi proje içerisinde __tests__ dizini oluşturalım ve bu dizin içerisine <Link> bileşeninin testini yazacağımız link.react.test.js dosyasını aşağıdaki gibi ekleyelim:

import React from 'react';
import renderer from 'react-test-renderer';
import Link from '../Link.react';
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});

Testin düzgün bir biçimde çalışması için package.json dosyasında scripts bölümünde test kısmını aşağıdaki gibi jest çalıştıracak hale getirelim:

"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "jest",
"eject": "react-scripts eject"
},

Ayrıca testte yazdığımız JSX syntax’ının da anlaşılması için babel.config.js dosyasını aşağıdaki gibi ekleyelim:

module.exports = {
presets:[
"@babel/preset-env",
"@babel/preset-react"
]
}

yarn test komutu ile çalıştırdığınızda aşağıdaki gibi snapshot dosyasının yazıldığına dair bir ibare ile karşılaşacaksınız. İlk adımda aslında sadece snapshot oluşturulmaktadır. Kıyaslama işlemi tekrar çalıştırdığınızda yapılır.

Komut sonrası __tests__/__snapshots__ dizini altında link.react.test.js.snap adında aşağıdaki gibi bir dosya oluşacaktır:

// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;

Gördüğünüz gibi react-test-renderer, onMouseEnter ve leave fonksiyonlarının detayını bırakmış ve sadece Function olarak ele almıştır. Zaten doğası gereği snapshot testi sadece arayüzde görünen kısımların test edilmesi için yapıldığından bu durumun oluşması doğaldır. yarn test komutunu tekrar çalıştırdığınızda Aşağıdaki gibi kıyaslamayı yapmış olacaktır.

Şimdi doğru çalışıp çalışmadığını kontrol etmek için test dosyasında link içeriğini facebook.com yerine example.com haline getirelim. Testi tekrar çalıştırdığınızda aşağıdaki gibi bir hata ile karşılacaksınız:

Sonuç olarak

Yazıda adımları uygulayarak ve Jest kütüphanesini kullanarak herhangi bir projedeki herhangi bir fonksiyonu test edebilir hale geldik. Bu adımlarda eksik/hatalı olduğunu düşündüğünüz kısımlar varsa bana ulaşabilirsiniz. Sonraki yazımda görüşmek üzere…

Kaynaklar

--

--