JavaScript’te Test Edilebilir Kod Yazmak ve Unit Test ile Sınamak

Uğur GELİŞKEN
11 min readDec 28, 2021

Her ne kadar iyi kod yazıyor olsanız da iyi bir projede yazdığınız fonksiyonların test edilmiş olması gerekmektedir. Bir kodu test ediyor olmak, onun doğru çalıştığını sadece sizin görmenizi için değil, o projenin tüm fonksiyonlarının sorunsuz olarak çalıştığını ispatlamak ve projenin dağıtılabilirliğine (deployment, publish) onay vermek içindir.

Bu makale serisinde öncelikle unit test, yani birim test kavramı konusunda bazı temel bilgilere değineceğiz. Öncelikle neden test yapmak istediğimiz konusunda kendimizi yeteri kadar ikna etmemiz gerekiyor. Yoksa sürekli olarak “yazdığım kod çalışıyor, niye buna test yazmak için vakit harcamam gerekiyor” düşüncesinden çıkamayabilirsiniz.

Birim (Unit) Test Kavramı

Birim testi, giriş ve çıkışı kontrol ederek programın bir bölümü (metodu, fonksiyonu) üzerinde bazı kodlar çalıştırır. Bu testlerle bir programın belirli alanlarında hataların nerede ve neden olduğunu görülebilir, kontrol edilebilir.

Birim test yazarken en önemli sorun; neyi test etmeye çalıştığınıza ve o kodun nasıl çalışması gerektiğine dair mantıksal bir anlayış kurmaktır. Hataları bulabilmek için, ilk olarak neyi aradığınızı bilmeniz gerekir.

JavaScript dünyasında, işler bir web sayfasının ön tarafında yorumlanarak çalıştığı için biraz daha karmaşıklaşır. Derlenen bir dil olsaydı, çok daha gelişmiş araçlarla testler yapılabilirdi. JavaScript’te unit testleri yapmanın en kolay yolu ayrı test framework’leri kullanmaktır. Mesela; Mocha, Jasmine, Karma gibi…

Çalışan Bir Kod Yine de Neden Test Edilmelidir

Bir fonksiyon yazdığımızda, normalde ne iş yapması gerektiğini düşünebiliriz. Yani fonksiyonun hangi parametrelerle hangi sonuçları verebileceğini tahmin edebiliriz.

Birim test kavramı, her bir koda ayrı ayrı testlerin yazıldığı anlamına gelir. Bir de entegrasyon testleri, fonksiyonel test ve uçtan uca uyumluluk testleri vardır, onlara şimdilik değinmeyeceğiz.

Geliştirme sırasında, fonksiyonu çalıştırarak ve sonucu beklenen ile karşılaştırarak fonksiyonu kontrol edebiliriz. Bu mantık, birim testin temelini oluşturur. Mesela Developer Tools’ta konsolda fonksiyonları test edebiliriz. Eğer bir şeyler yanlışsa kodu düzeltip tekrar çalıştırırız, tekrar kontrol ederiz… Bu süreç sonuçtan tamamen tatmin olana kadar sürer gider. Ancak bu tür manuel tekrar çalıştırmalar hatalı bir yöntemdir. Çünkü kodlar manuel olarak test edilemez, mutlaka gözden bir şeyler kaçar.

Mesela X ve Y adında bir fonksiyonlar tanımladık. X fonksiyonu çalışıyor ama Y fonksiyonu çalışmıyor. Y kodunu düzelttik. Artık her iki fonksiyon da çalışıyor. Peki, kontrolümüz tamam mı? Hayır, X fonksiyonu tekrar kontrol etmeniz gerekir, çünkü Y fonksiyonunda yaptığınız düzeltme belki X fonksiyonu etkiledi.

Bu yaşanan olay normalde tipik bir olay, yani çokça yaşanır. Bir fonksiyon geliştirdiğimizde, aklımıza gelebilecek bazı olası kullanım senaryolarını göz önünde bulundurarak kendimizce önlemler alırız. Hata ayıklama, hata işleme fonksiyonlarına yönlendirme, konsol mesajları vs. Fakat bir yazılımcıdan her türlü olasılığı düşünmesi ve her durum için kırılmaz bir kod yazmasını istemek anlamsız ve imkansızdır.

Yine yazdığı kodları da her dağıtım öncesinde tek tek kontrol etmesini istenemez. Bu bütün süreci otomatik olarak baştan sona yapacak, her bir fonksiyonu test edip raporlayacak bir sisteme ihtiyaç duyulur. Buna da otomasyon denir.

Teknikler ve Stratejiler

Her fonksiyon için birim test yazılması zorunlu değildir, bunun kararını da iyi vermek gerekir.

Birim test yazmak, pratik gerektirir ve ne kadar çok test yazarsanız o kadar çok anlarsınız. Her yazdığınız testte yeni yeni metotlar keşfedebilirsiniz. Öğrenme sürecinde Open Source olarak dağıtılan projelerin testlerini incelemek oldukça faydalıdır.

Neyi test edeceğimizi öğrendikten sonra bir test yazarak çalışmaya başlamak isteriz, ama tam olarak neyi test etmelisiniz?

Eğer bir kodu test edemiyorsanız, o kod yanlış yazılmıştır.

En yaygın yöntem parametre girdileri veya kullanıcı olaylarından biridir. Bir kullanıcının oturum açtığını ve Cookie ile bazı değerleri saklamak istediğimizi varsayalım. Cookie kayıt işlemi, birden çok sonuca (girişin başarısızlığı ve başarısı) dayanılarak test edilmesi gereken belirli bir davranıştır. Mesela kullanıcı sayfanın Cookie yazmasına izin vermemiş olabilir. Veya yazılsa bile güvenlik programları bu Cookie değerlerini güvenli görmeyip silebilir.

Bir fonksiyon için birden fazla test yazılabilir. Her test çıktısı, sorunlu veya hatalı alanları bulmanıza yardımcı olacak sonuçlar içerir.

Hata raporlarında şu soruların cevapları aranmalıdır:

  • Hangi özelliği veya sorunu test ediyorsunuz?
  • Testi yaptıktan sonra çıktı ne oldu?
  • Başarılı bir test için beklenen çıktı veya çıktılar nelerdir?

Birim test yazarken mümkün olduğunca basit tutulur. Yani karmaşık işle yaptırılmaz. Eğer karmaşık işler yaptırılması gerekiyorsa, her bir durum için ayrı ayrı testler yazılmalıdır.

Bir birim testine baktığınızda en fazla 5 saniyede onun neyi test ettiğini anlayamıyorsanız, o birim testini kullanmayın, silin yeniden yazın.

Yeni bir test yazarken kodunuzu her zaman çok basit tutmaya çalışın. Basit kalmanın en iyi yolu, neyi kontrol ettiğinizi ve ne tür bir çıktı beklediğinizi göz önünde bulundurarak net bir fikre sahip olmaktır.

Mocha ve Chai ile Birim Test Yazmak

JavaScript için birim test yazmak için şu an popüler olarak Mocha, Jasmine, AVA, Tape, Jest, Puppeteer, QUnit, Intern, Chai, Cucumber gibi araçlar kullanılabilir.

Kitapta, yeni başlayanlara da uygun olması amacı ile basit ve güçlü bir JavaScript birim test framework’ü olan Mocha’yı kullanacağız. Zaten en yaygın olarak kullanılanı da Mocha’dır. Mocha’yı kullanırken de Chai’den destek almamız gerekecek.

Node.js Kurulumu

Günümüzde JavaScript ile birim testler yapılırken Node.js üzerinden test yürütülmektedir. Bu nedenle ilk olarak Node.js kurulumunu yapmak gerekmektedir.

Node.js kurulumunu yapmak için sırasıyla aşağıdaki adımları uygulayın.

İşletim sisteminizin Windows 64x olduğunu düşünerek ilerleyelim. https://nodejs.org/ adresine girip güncel olan sürümü indirebilirsiniz. Eğer Linux kullanıyorsanız Package Manager’dan yükleyebilirsiniz.

Kurulum aşamasında sözleşmeyi onaylayın ve Next ve Install butonları ile kurulumu varsayılan ayarlarla ilerletin. Kurulum tamamlandıktan sonra komut satırını açın ve node -v komutu ile test edin (v14.3.0).

Mocha

Mocha, JavaScript birim test framework’üdür. Node.js ve tarayıcı üzerinde çalışır. Node.js üzerinde kurulumu için aşağıdaki komut uygulanır.

npm install --save-dev mocha

Chai

Chai

Chai, BDD (Behavior Driven Development) ve TDD (Test Driven Development) onaylama kütüphanesidir. Herhangi bir JavaScript test Framewok’ü ile eşleştirilebilir. Biz de Mocha ile eşleştireceğiz. Chai, okunabilir, etkili ve güçlü bir dil sağlar.

npm install --save-dev chai

Yeni Proje Başlatmak

Projeyi başlatmak için terminal komutlarını kullanacağız. Çünkü proje Node.js üzerinde çalıştırılacak.

Projenizi hazırlamak istediğiniz dizine gidin ve terminali açın. Ardından npm init komutunu girin. Komutu girdiğinizde sizden projeyi tanımlamanız için gerekli bilgiler istenecek, isterseniz bu bilgileri girin isterseniz de Enter tuşu ile ilerleterek varsayılan değerlerle bırakıp ilerletin. Son olarak size ayarlar gösterilecek ve bu ayarlarla package.json dosyasının oluşturulması için onay istenecek. Onay vermek için yes yazıp Enter tuşu ile onaylayın.

Onayladıktan sonra klasör içinde package.json dosyası oluşturulacaktır.

Ardından aşağıdaki komut ile Mocha ve Chai kurulumlarını yapın.

npm install --save-dev mocha chai

Klasör dizininde node_modules klasörü açılacak ve bunun içine Mocha ve Chai dosyaları indirilecek. Aynı anda package.json dosyasında da devDependencies içine proje bağımlılığı olarak versiyonları ile birlikte eklenecek.

Ek bir işlem daha yapacağız; package.json dosyasında test karşılığına mocha yazarak düzenleyin ve kaydedin. Dilerseniz diğer değerleri de düzenleyebilirsiniz. Burada önemli olan main değeri. Bu değer, kök JavaScript dosyasını gösterir. Genelde index.js, app.js, main.js gibi değerler verilir. Böylece npm test komutu uygulayıp test otomasyonunu başlattığımızda Mocha devreye girecek.

{
"name": "mocha_unit_test",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "mocha"
},
"author": "",
"license": "ISC",
"devDependencies": {
"chai": "^4.2.0",
"mocha": "^7.2.0"
}
}

Module’ler için Birim Testlerin Yazılması

Örnek test kodumuzda bir modül oluşturacağız. Modülde temel olarak dört işlem yapacağız. Proje klasörümüzde (Örneğimizde mocha_unit_test klasörü kullanılıyor), index.js dosyası oluşturuyoruz. Bu dosya ismi, package.json’da tanımladığımız main dosya ismidir.

index.js

module.exports = {    init: () => {
return "Program başladı...";
},

sum: (x,y) => {
return x + y;
},
diff: (x,y) => {
return x - y;
},
divide: (x,y) => {
return x / y;
},
multiple: (x,y) => {
returrr x * y;
}
}

Modülümüzde init, sum, diff, divide ve multiple isimlerinde metotlarımızı tanımladık. Kodları incelediğinizde her birinin basitçe bir işlem yapıp değer return ettiğini göreceksiniz. Ancak dikkat edin, multiple metodunda bir yazım yanlışı var; return yerine returrr yazdım. Bu hatayı bilerek yaptık, test aşamasında nasıl görüneceğini izleyeceğiz. Şimdi bu metotları sınayalım…

Genel olarak birim test framework’leri test yapacağı yönergeleri izlemek için kodların olduğu dosya ile aynı isme sahip belli formatlardaki dosyaları tarar ve sırayla işler. Bu yapıyla index.js dosyasının test dosyasının index.test.js olmasını bekler. Veya test klasörü içinde index.test.js olmasını bekler. Biz, test klasörü içinde ../test/dosyaIsmi.test.js formatı ile ilerleyeceğiz.

Aşağıda, her bir metodu test eden kodlarımız yer alacak.

../test/index.test.js

const assert = require('chai').assert;// Referanslar tanımlanıyor
const init = require('../index').init;
const sum = require('../index').sum;
const diff = require('../index').diff;
const divide = require('../index').divide;
const multiple = require('../index').multiple;
describe("Index başlangıç testi", () => {
it("Program başladı... mesajının gelmesi gerekiyor", () => {
assert.equal(init(), "Program başladı...");
});
});
describe("Toplama işlemi testi", () => {
it("3+5 işlemi sonucu 8 gelmeli", () => {
assert.equal(sum(3,5), 8);
});
});
describe("Çıkarma işlemi testi", () => {
it("8-2 işlemi sonucu 6 gelmeli", () => {
assert.equal(diff(8,2), 6);
});
});
describe("Bölme işlemi testi", () => {
it("8/2 işlemi sonucu 4 gelmeli", () => {
assert.equal(divide(8,2), 4);
});
});
describe("Bölme işlemi 0'a bölme testi", () => {
it("8/0 işlemi sonucu Infinity gelmeli", () => {
assert.equal(divide(8,0), Infinity);
});
});
describe("Çarmpa işlemi testi", () => {
it("8*2 işlemi sonucu Infinity gelmeli", () => {
assert.equal(multiple(8,2), 16);
});
});
describe("Çarmpa işlemi 0 testi", () => {
it("8*0 işlemi sonucu Infinity gelmeli", () => {
assert.equal(multiple(8,0), 0);
});
});

Yazdığımız kodları inceleyelim ve biraz Mocha ve Chai hakkında bilgi edinelim…

İlk satırda ilk olarak node_modules içinden chai modülünü çekip assert nesnesi oluşturuyoruz. Assert, ileri sürmek anlamına gelen iddiaları oluşturmak için kullanılır. Yani “iddia ediyorum ki şu şu parametreleri verirsem şu metot şu sonucu döndürür” mantığı ile test yazabilmemizi sağlar.

Sonraki satırlarda da require() metodu ile index.js’te yer alan metotları tek tek çekip aynı isimde sabitlere atadık. Artık bu referans isimleri ile test aşamalarını gerçekleştirebileceğiz.

Her bir test describe() metodu ile yazılır. İki parametre alır. Bunlardan ilki String olarak o testi tanımlayan cümleciktir. Mümkün olduğunca anlamlı ve kısa yazılır. İkinci parametresi de bir fonksiyondur.

Fonksiyon içinde it() metodu kullanılarak denemeler yapılır. Bu metot da İki parametre alır. Birinci parametre, o testte yapılması gereken denemenin tanımını belirten String ifadedir. Bunu da mümkün olduğunca kısa ve anlamlı tutmak gerekir. İkinci parametresi de yine bir fonksiyondur.

İkinci fonksiyon içinde de assert nesnesinin equal (eşittir) metodunu kullandık. Bu metot iki parametre alır. Birinci parametre test edilecek fonksiyon ve parametreleri, ikinci parametre de o fonksiyonun döndürmesi ön görülen değeri belirtir. Mesela sum(3,5) için 3+5 işleminin eğer fonksiyon doğru çalışıyorsa 8 olarak gelmesini beklediğimizi söylüyoruz.

Testlerimizi yazdıktan terminalden npm test komutunu girip Enter ile onaylayıp test sürecini başlatın.

Mocha, test sürecine başlar başlamaz kod içerisinde bir yazım hatası buldu. Verdiği rapora göre index.js’te 20. satırda returrr x * y; satırında bir sorun var diyor. Şimdi o satırı return x * y; olarak düzeltip bir daha deneyelim.

Yazdığımız testler sırası ile işlendi ve tanımladığımız koşullara göre sonuç döndürdü. Toplamda 7 test yapılmış ve hepsi de passing tanımıyla 9 milisaniyede testten geçmiş. Şimdi bu testlerden birini hatalı tanımlayalım. Mesela “Bölme işlemi 0’a bölme testi” isimli birim testte 8/0 işleminin sonucunu 0 olarak beklediğimizi belirtecek şekilde düzenleyip testi tekrar başlatalım.

describe("Bölme işlemi 0'a bölme testi", () => {
it("8/0 işlemi sonucu 0 gelmeli", () => {
assert.equal(divide(8,0), 0);
});
});

Sonuca baktığımızda 6 adet geçerli test, 1 adet geçersiz test olduğu görülüyor. İşlem sonucunun 0 değil, Infinity (sonsuz) olması gerektiğini söylüyor. Bu haliyle Mocha ve Chai bizi test sürecinden geçirmiyor ve eğer proje derlenecekse (React veya Angular gibi projeler test işleminden sonra derlenir) süreci durdurur ve hataların düzeltilmesini bekler.

Chai Test Fonksiyonları

Önceki konumuzda yer alan örneğimizde sadece equal ile eşitlik sınamaları yaptık, ancak Chai’de daha farklı ve gelişmiş kontroller de bulunmaktadır. Bunların en çok kullanacaklarımızı sırası ile incelemeyelim bu metotlarla örnek testler yazmaya çalışalım…

Babel

Class testleri yazabilmeniz için Babel modülünün de kurulu olması gerekmektedir. Node.js’de ES6 kodunu ES5'e çeviren bir transpiler olan Babel kullanılır. Aşağıdaki komut ile kurulumunu yapabilirsiniz.

npm install --save-dev babel

Karşılaştırma Metotları

equal: Eşitlik değil sınaması yapar.

assert.equal(gercek_deger, beklenen_deger, 'mesaj');

notEqual: Eşit değil sınaması yapar.

assert.equal(gercek_deger, beklenen_deger, 'mesaj');

isAbove: Büyük sınaması yapar.

assert.isAbove(kontrol_edilecek_deger, üstünde_kalacak_olan_deger, 'mesaj');

isAtLast: Büyük eşit sınaması yapar.

assert.isAtLast(kontrol_edilecek_deger, en_az_olacak_deger, 'mesaj');

isBelow: Küçük sınaması yapar.

assert.isBelow(kontrol_edilecek_deger, altında_kalacak_olan_deger, 'mesaj');

isAtMost: Küçük eşit sınaması yapar.

assert.isBelow(kontrol_edilecek_deger, en_çok_olacak_olan_deger, 'mesaj');

Mantıksal Karşılaştırma Metotları

isTrue: Dönen değer sonucu true mantıksal değeri mi diye sınar.

assert.isTrue(kontrol_edilecek_deger, 'mesaj');

isFalse: Dönen değer sonucu false mantıksal değeri mi diye sınar.

Veri Tipi Kontrol Metotları

isNull: Sınanan değer boş mu diye kontrol edilir.

assert.isNull(kontrol_edilecek_deger, 'mesaj');

isNaN: Sınanan değer bir sayı değil mi diye kontrol edilir.

assert.isNaN(kontrol_edilecek_deger, 'mesaj');

exist: Sınanan değer var mı diye kontrol edilir.

assert.exist(kontrol_edilecek_nesne, 'mesaj');

notExist: Sınanan değer yok mu diye kontrol edilir.

assert.noExist(kontrol_edilecek_nesne, 'mesaj');

typeOf: Sınanan değerin veri türü kontrol edilir. Veri türü olarak string, number, array, object, function gibi tanımlamalar yapılabilir.

assert.typeOf(kontrol_edilecek_deger, 'veri_türü', 'mesaj');

Öğrenmiş olduğumuz bu başlıca sınama türlerini yeni bir örnek ile inceleyelim…

Class’lar için Birim Testlerin Yazılması

Örnek proje olarak bir geometrik şekil için belirli aralıklarla ölçüler tanımlayan bir Class oluşturalım (Önceki örneğimizde Module kullanmıştık, bunda farklı olsun).

Class’ımız, bir dikdörtgen için yükseklik, genişlik ve derinlik ölçüleri üretecek; bu değerlerle de hacim hesabı yapacak.

Yine bir klasör içinde npm init komutu ile proje başlatın ve gerekli ayarlamaları bir önceki örnekte olduğu gibi devam ettirerek package.json dosyasını oluşturun.

Bu sefer Class ile çalışacağımız için Class isimlendirmesine uygun olarak dosyamızı oluşturalım. Örneğimizde Square adlı bir sınıf tanımlayacağız (Sınıf dosya isimleri büyük harfle başlar).

Square.js

class Square {    constructor(a,b,h){
this.a = this.generateA(a);
this.b = this.generateB(b);
this.h = this.generateH(h);
}
generateA(max) {
return Math.ceil(Math.random() * max + 1);
}
generateB(max) {
return Math.ceil(Math.random() * max + 2);
}
generateH(max) {
return Math.ceil(Math.random() * max + 6);
}
calcuateVolume() {
return this.a * this.b * this.h;
}
getA () { return this.a };
getB () { return this.b };
getH () { return this.h };
}module.exports = Square;

Hazırladığımız Class’ımız 3 parametre alıyor. Bunlar dikdörtgenimizin genişlik, uzunluk ve derinlik değerlerini tutan a, b ve h parametreleri. Ancak Square Class’ımız bu parametre değerlerini azami değer olarak alıyor ve belli bir işlemden geçirerek yeni ölçüler üretiyor. Mesela h değeri için aldığı azami değer ile 0–1 arası rastgele bir değeri çarpıp, minimum da 6 olacak şekilde bir ölçü belirliyor. Mesela parametre değeri 2 ise; 2–8 arası bir değer üretiliyor.

Hacim hesabını da calculateVolume metodu ile yapıyoruz. Her bir parametre değeri çarpılıp return ediliyor. Her bir a, b ve h değerini de almak için getA, getB, getH metotlarını tanımlıyoruz.

Son olarak bu Class’ı dışa aktarabilmek için modue.exports = Square; ile dışa aktarıyoruz.

Proje klasörümüzde ../test klasörü içinde Square.test.js dosyamızı oluşturup testlerimizi yazalım…

../test/Square.test.js

const assert = require("chai").assert;let Square = require('../Square');describe("Square", function () {  let square;  before(() => {
square = new Square(1, 2, 15);
});
describe("Minimum değer kontrolleri :", function () { it("A minimum geçerli değerini kontrol et", function () {
assert.isAbove(square.getA(), 1, "A uzunluğu 1'den büyük.");
});
it("B minimum geçerli değerini kontrol et", function () {
assert.isAbove(square.getB(), 2, "A uzunluğu 2'den büyük.");
});
it("H minimum geçerli değerini kontrol et", function () {
assert.isAbove(square.getH(), 6, "A uzunluğu 6'den büyük.");
});
}); describe("Hacim hesabı:", function () { it("Hacim 12 metre küpten büyük olmalı", function () {
assert.isAbove(square.calcuateVolume(), 12, "Hacim hesabı geçerli");
});
it("Hacim (1+1)*(2+2)*(15+6) metre küpten küçük olmalı", function () {
assert.isBelow(square.calcuateVolume(), 168, "Hacim hesabı geçerli");
});
it("Hacim yapan fonksiyon var mı", function () {
assert.exists(square.calcuateVolume(), "Hacim hesabı yapılabilir");
});
it("Hacim değeri sayı olmalı", function () {
assert.typeOf(square.calcuateVolume(), "number", "Hacim hesabı geçerli");
});
}); after(() => console.log("=> Testler bitti."));});

Bu örneğimizde biraz farklı tarzda testleri yazdık. İlk olarak iç içe describe() metodu kullandık. Yani bu da demektir ki; aynı kategorideki sınamalarınızı bir grup altında toplayabilirsiniz. Sınamalarımızı yaparken de klasik fonksiyon yapısını kullandık.

before() ve after() metotlarını da yeni kullandık. Bunlar; test sürecinin başlaması ve bitişi ile çağırılacak olan yaşam döngüsü (lifecycle) metotlarıdır. Zorunlu değiller, ancak bazen test başlarken veya biterken işlemler yapmak istediğinizde lazım olabilirler. Örneğimizde before() metodunda square adında yeni bir Sqauare Class objesi oluşturduk, sınamalarımızda da bu objeden faydalandık. Objemizi oluşturup parametreleri verdikten sonra da sırasıyla değer kontrolü yaptık.

Sınamaları yaparken, değerlerin minimum kontrollerini, hacim hesabı için gerekli olan metodun var olup olmadığını, hesaplanan hacim için en az ve en çok metreküp ölçü değeri kontrolünü sınadık.

npm test komutu ile testimizi başlatalım.

Testimizi yorumladığımızda 2 grup altında toplamda 7 adet testimiz toplamda 12 milisaniyede başarılı bir şekilde gerçekleştirildi, kodlarımız testi geçti.

Chai metotları hakkında daha fazla bilgi almak için https://www.chaijs.com/api sayfasından referansları inceleyebilirsiniz.

--

--

Uğur GELİŞKEN

Full-stack Developer [ UI / UX | JAM Stack | ME(A,R,V)N | LAMP ], Author, Pro Gamer