React Tutorial #2: Unit Test & Test Coverage (Mocha + Enzyme + Chai + Sinon + Istanbul)

Mert Özkarakoç
8 min readJan 5, 2019

--

Unit test yazma mevzusu her projede “kesin yazalım” ile “ama vaktimiz az" arasında gidip gelen ve genelde ikinci seçeneğin karar olarak alındığı bir konu. Bu yazıda, React ile geliştirdiğimiz uygulamalar için unit testleri nasıl yazabileceğimiz konusuna değineceğiz. Yazının konusu test araçları ve kütüphaneleri olduğundan ve de zaten yazılmış bir proje ile çalıştığımızdan testleri mevcut projeye göre yazacağız.

Önce “unit test” kavramına kısa bir tanım yapalım. Unit test, yazdığımız ya da yazacağımız kodları test etmek için yazdığımız test kodları diyebiliriz. Yazının konusu olan React üzerinden örnek verecek olursak projemize yazdığımız bileşenler, sayfalar ve proje içerisinde kullandığımız yardımcı fonksiyonların hepsi bir birimdir ve bu birimlerin fonksiyonalitesinin doğru çalıştığını test etmek için unit testler oluşturacağız.

Bu bölümde, önceki yazıda oluşturduğumuz proje üzerinden devam edeceğiz. Testlerimizin hepsinin başarılı olabilmesi için kodlarda bazı ufak değişiklikler yaptım. Önceki yazıda oluşturduğumuz src klasörünün yeni haline ve bu yazıda anlatılan geliştirmelere GitHub’dan erişebilirsiniz.

İlk kütüphanemiz: Mocha. Mocha bir test framework’ü. Testleri tanımlama ve çalıştırma aşamalarında kullanacağız. Önce projemize ekleyelim ve daha sonra nasıl kullanılacağını anlamaya çalışalım.

npm install mocha eslint-plugin-mocha --save-dev

Mocha’nın yanında yine bir ekstra eklenti kullandık. Çünkü önceki yazıda tanımladığımız ESLint, mocha’nın sağladığı API’leri anlamıyor ve bunu bir lint hatası olarak görüyor. Mocha ile gelen fonksiyonların ne olduğuna birazdan değineceğiz ama önce bu hatayı giderebilmek için ESLint’in konfigürasyon dosyasına, .eslintrc.js içerisine birkaç ekleme yapalım. env property’sine mocha:true tanımını ben önceki yazıda yanlışlıkla yapmışım. :) plugins kısmındaki diziye 'mocha' eklentisini kullanacağımızı belirtiyoruz.

plugins: ['babel', 'import', 'jsx-a11y', 'mocha'],

Mocha’yı projemize dahil ettik, şimdi sıra konfigüre etme kısmına geldi. Mocha kütüphanesi CLI’i ile birlikte geliyor ve çalıştırmak için proje dizininde terminale mocha yazmamız yeterli; fakat bu kütüphanenin birçok opsiyonel özelliği var ve bunları sırası ile -- belirteci ile argüman olarak geçmemiz gerekecek. Bu da oluşturacağımız script’in gereksiz uzun olmasına neden olacak. O yüzden mocha’nın çalışırken alacağı parametreleri ayrı bir dosyadan, mocha.opts dosyasından alacağız. Bu dosyayı proje dizinimizde test klasöründe oluşturalım ve içeriğini aşağıdaki şekilde güncelleyelim.

--require @babel/register
--require ignore-styles
--reporter spec
--recursive
test/utils/setup.js

Yine, yeniden bir takım yardımcı araçlar… Bu araçları da projeye dahil edelim.

npm install @babel/register ignore-styles --save-dev

Babel’i önceki yazıda tanımlamıştık. Node.js çalışma ortamımızda, testleri çalıştırırken babel ile entegre olabilmek için babel-register kütüphanesini projemize ekledik.

Bir diğer eklentimiz ignore-styles ise proje içerisinde kullandığımız stil dosyalarının çalışma anında hata vermemesini sağlıyor. Neden çalışma anında hata verecekti? Çünkü testlerimizi node.js ile çalıştıracağız ve bu platformda CSS’e yer yok. :) Özetle, linkte belirttiğim stil dosyalarının import işlemlerinin göz ardı edilebileceğini belirtmek amaçlı olarak kullanıyoruz.

reporter argümanını ise mocha ile testleri çalıştırırken ekranda çıktıların nasıl görüneceğini belirlemek için kullanıyoruz. Mocha’nın çeşit çeşit reporter tipleri var. Hangisini beğenirseniz artık. :) Testler çoğaldığında ben dot versiyonunu kullanıyorum ama bu yazıda çok fazla testimiz olmayacağından spec olarak tercih ettim.

recursive argümanı ile testlerin alt klasörlerde de rekürsif olarak devam edebileceğini belirtiyoruz. Son satırda yazdığımız test/utils/setup.js ile testlerden önce çalışacak, testlere gerekli ortamı hazırlayacak javascript dosyamızı çağırıyoruz. Bu dosyayı test/utils klasöründe oluşturup içeriğini aşağıdaki şekilde güncelleyelim.

Bu dosya içerisinde import ettiğimiz diğer JavaScript dosyalarından ilki dom.js dosyasını aynı klasör içerisinde oluşturup içeriğini aşağıdaki şekilde güncelleyelim.

Yukarıda, testleri node.js ile çalıştıracağımızı belirtmiştik. Bir frontend kütüphanesini, tarayıcıda çalışacak bir uygulamayı, serverside test etmek istiyoruz. Tarayıcıdan erişebileceğimiz DOM API’lerini sunucu tarafında simüle edebilmek, yani node.js içerisinde DOM oluşturmak için jsdom kütüphanesini kullanıyoruz. dom.js içerisinde jsdom ile bir DOM oluşturduk ve global değişkenlerde kullanabileceğimiz diğer özellikleri burada tanımladık. Şimdi diğer dosyamıza, console.js ye geçebiliriz.

Buradaki amacımız ise console global değişkenindeki error methodunu değiştirmek. React uygulaması geliştirken, bir özelliğe propTypes içerisinde tanımladığınız tipten farklı bir tipte değer verdiğimizde ya da required olan bir property’i bileşene vermediğinizde tarayıcıda console’da Warning: failed propType: .... ile başlayan bir uyarı görürüz. Bu uyarı React propType validasyonlarında console.error ile veriliyor fakat bir hata fırlatılmıyor. Dolayısı ile bu tip hatalı kullanımlar testlerimizde yakalanmıyor. Biz de böyle kullanımlarda hata fırlatılmasını ve testlerin başarısız olmasını istediğimiz için console.error methodunu değiştirdik. console kullanımı ESLint kurallarından dolayı hata verdiği için dosyanın başına bir eslint-disable satırı ekledik. Bir sonraki dosyamız: init.js

Burayı, yazının ana konularından biri olan enzyme kütüphanesini konfigüre etmek için kullanıyoruz. enzyme Airbnb tarafından geliştirilen bir başka JavaScript test kütüphanesi. React bileşenlerini test edebilmemizi sağlayan, sempatik bir araç. Testlerden önce konfigüre edilmeye ve bir adapter'e ihtiyaç duyuyor. Her testin başında tekrar tekrar konfigüre etmemek için bu dosyayı kullanıyoruz. enzyme ile ilgili daha detaylı açıklama yapacağız fakat öncesinde şimdiye kadar bahsettiğimiz araçları projeye dahil edelim.

npm install enzyme enzyme-adapter-react-16 jsdom --save-dev

Bu javascript dosyalarını projeye dahil ettikten sonra, “devDependency” olarak eklediğimiz paketleri, proje içerisinde kullandığımız için ESLint 'jsdom' should be listed in the project's dependencies, not devDependencies şeklinde bir hata veriyor. Aslında güzel bir hata, bizi import ettiğimiz bir paketi dependency olarak kullanmadığımızda, production kurulumlarında hata almamak adına uyarıyor. Fakat biz burada ne yaptığımızdan eminiz. Sadece testlerde kullanacağımız bir import işlemi yaptık. Bu yüzden hatayı önlemek için .eslintrc.js içerisinde rules kısmında bu kuralı düzenleyebiliriz. Rule kısmına aşağıdaki kuralı ekliyoruz.

'import/no-extraneous-dependencies': ["error", { "devDependencies": true }]

Hata giderildi. Şimdi tekrar enzyme konusuna dönebiliriz. Enzyme, Airbnb tarafından geliştirilen bir React test kütüphanesi. React ile geliştirdiğimiz bileşen ve sayfalarımızı test edebilmek için birçok API sunuyor: shallow, mount ve render API’lerinin hangi amaçla kullanıldığını açıklayıp örnek üzerinden daha anlaşılır kılmaya çalışalım.

Bir bileşeni, bileşen içerisindeki diğer bileşenlerin davranışlarından bağımsız test etmek istediğimiz zaman shallow API’sini kullanabiliriz. Örneğin, bileşen doğru elementlerden oluşuyor mu, istediğimiz stil özelliklerine sahip mi? Bir özelliğini değiştirdiğimizde beklediğimiz davranışı sergiliyor mu? Gibi soruların cevaplarını alabilmek için shallow özelliğini kullanabiliriz. Shallow API’sinde bileşenler DOM’da mount edilmez, daha yüzeysel bir test instance’ı sağlar.

mount, shallow API’sinden farklı olarak bir bileşeni child elementleri ile birlikte DOM üzerinde çalıştıyor. Full DOM Rendering olarak adlandırılan bu yöntemi, bir bileşen DOM ile etkileşime geçtiğinde kullanabiliriz. Örneğin, bir bileşenimizde input elementi var ve DOM üzerinden bu elementi bularak bu değerini değiştiriyoruz. Bu işlemleri test edebilmek için mount API’sini kullanabiliriz. mount ile yazacağımız testlerde bileşenler, yukarıda jsdom ile oluşturduğumuz yapay DOM üzerinde oluşacak.

render methodu ise bir elementin HTML olarak oluşturacağı string’i veriyor. Bu method’dan oluşan çıktıyı shallow ve mount ile oluşturduğumuz bileşenlerden .html() methodu ile de alabiliyoruz. O yüzden bu methodu çok kullanmayacağız.

Mocha ve Enzyme’i tamamladığımıza göre ilk testimizi yazabiliriz. Testleri test klasöründe toplamak yerine her bileşenin yanına *.test.js olarak eklemeyi tercih ediyorum. Bu yazıda da her bileşenin yanına unit testlerimizi ekleyeceğiz. İlk olarak, todoFooter.js bileşenimizin testini yazalım. todoFooter.test.js dosyamızı bileşenin yanına ekleyelim ve içeriğini aşağıdaki şekilde güncelleyelim.

Testleri tanımlamadamocha kütüphanesinin bize sağladığı describe ve it fonksiyonlarını kullanacağız. Bu fonksiyonlar, BDD konsepti üzerine hazırlanmış, bileşenlerimizin nasıl davranması gerektiğini tanımlamıza yardımcı olacaklar. describe ile testimizi tanımlıyoruz, todoFooter bileşenini test ettiğimizi tanımladıktan sonra, it ile beklediğimiz davranışları sıralıyoruz.

TODO MVC projesi daha önceden yazılmış olduğu için, başlangıçta da söylediğim gibi mevcut kodlardan gereksinimleri çıkardım ve testleri öyle yazdım. :) İlk testimiz, it('should render a <footer> with "footer" class', () => {...}) ilk parametrede davranışımızı tanımladık ve ikinci parametrede, callback parametresi olarak testimizi yazdık. Bu callback fonksiyonunda bir hata oluşursa testimiz başarısız sayılacak. Bu testte bileşenin bir html <footer> elementinden oluşması gerektiğini ve stil olarak footer class’ına sahip olmasını bekliyoruz. shallow ile bu testi yazabiliriz. Bileşenimiz shallow methoduna parametre olarak veriyoruz ve çıktısı üzerinde testlerimizi yapıyoruz.

Bileşenin beklentilerimizi karşılayıp karşılamadığını tespit edebilmek için bir assertion kütüphanesine ihtiyacımız var. Bu noktada chai seçeneklerden biri. Chai bir test kütüphanesi ve davranışsal sonuçları tespit etmemizde yardımcı oluyor. Üç temel API’si, assert expect ve should birbirine benzer çözümler sağlıyor. Sadece kullanım stilleri biraz farklı. assert yukarıda kullandığımız gibi, assertion işlemini yaparak neyi karşılaştıracağımızı belirliyor. assert ile yapılabilecek işlemlere API referansından erişilebilir.

expect ve should ise daha davranışsal test yöntemlerine uygun API’ler. expect bir fonksiyon referansı ve içerisine parametre olarak test etmek istenilen obje’yi alıyor. Daha sonra test senaryomuzu yazmaya başlıyoruz. Örnek vermek gerekirse; expect('foo').to.be.a('string') şeklinde bir kullanımı var. should ise obje’lerin prototipine expect ile kullanılabilecek özellikleri ekliyor. Yine basit bir örnek üzerinden inceleyecek olursak 'foo'.should.be.a('string') şeklinde bir kullanımı var. Kullanım şekillerini ve farklılıklarını detaylı olarak inceleyebilirsiniz, biz chai kütüphanesini projeye dahil edelim ve örnekler üzerinden incelemeye çalışalım.

npm install chai --save-dev

İlk testimizde shallow ve assert kullanarak bileşenimizin footer elementi içermesi gerektiğini ve footer class’ına sahip olması gerektiğini test ettik. İkinci testimizde describe içerisinde farklı bir test tanımı yapıp count prop’unu test ettik. İkinci adımda benzer bir işlem yaptığımız için bu kısmı detaylandırmıyorum. İkinci testte, describe içinde describe ile yeni testleri tanımlayabildiğimizi aktarmış olalım. Üçüncü testte ise fonksiyon tipindeki bir prop’u test ediyoruz. Bileşenin alt kısmında yer alacak olan filtreleme alanının doğru çalıştığını test etmek istiyoruz.

TodoFooter bileşenimizin filtreleme alanından hangi tipin seçildiğini handle edebilmesini ve bunu container’a doğru şekilde iletebilmesini bekliyoruz. TodoFooter bileşeni handleFiltering isimli bir prop almalı ve filtrelerden herhangi birine tıklandığında, bu prop’u çağırıyor olmalı. Testimizi yazalım ve açıklamaya çalışalım.

İlk testimizde önce filtre aksiyonlarının doğru şekilde yer aldığını test ediyoruz. filters class’ına sahip bir ul elementi var mı? İkinci testte ise bu alandaki linklere tıklandığında handleFiltering prop’u çağırılıyor mu? Tıklandığında? Kod içinde bunu nasıl test ediyoruz açıklamaya çalışalım.

should handle filtering testinde, daha önceki testlerde kullanmadığımız bir kütüphane var: sinon Keşke ben yazmış olsaydım diyebileceğim bir türden kütüphane. :) Yine bir çok API sağlıyor. spy,stub,mock,fakeTimer en çok kullanılan API’leri sanırım. Kısaca özetlemeye çalışalım.

spy bir fonksiyon referansı olarak kullanılıyor ve üzerinde, fonksiyonun hangi parametrelerle çağırıldığı, ne döndüğü, kaç kez çağırıldığı gibi bilgileri kaydediyor.

stub da bir fonksiyon referansı. Spy’dan farklı olarak oluşturduğumuz referansın nasıl çalışacağını da biz belirliyoruz ve bunu mevcutta bulunan herhangi bir fonksiyon yerine kullanabiliyoruz. Örneğin bir bileşenimizde, dışarıdan bir fonksiyon çağırıyoruz ve testlerimizin o fonksiyondan etkilenmesini istemiyoruz ya da o fonksiyonun tüm davranışlarını test edebilmemiz mümkün değil. Bir stub oluşturarak o obje’ye inject edebiliriz. mock ise bildiğimiz mock. :)

Son olarak fakeTimer javascript kodlarımızda kullandığımız setTimeout gibi zamana bağlı async çalışan kodlarımızı senkron hale dönüştürmede kullanılıyor. Yani setTimeout içerisinde yazdığımız bir kodu, timeout süresince beklemeden çalıştırabilmek için fakeTimer kullanabiliriz.

Bu projede, bütün API’leri kullanacak senaryoları oluşturamadım ama genel bir bilgi mahiyetinde açıklamak istedim. Temel kullanıma aşina olduktan sonra sinon dokümantasyonundan öğrenmesi çok keyifli. :)

npm install sinon --save-dev

Bileşenimizde handleFiltering prop’unun doğru çalışıp çalışmadığını test edecektik. Linke tıklandığında, bir kez çağırılması ve hangi linke tıklandığı bilgisinin gelmiş olmasını bekliyoruz. Çağırılma sayısını ve parametreleri test edeceğimiz için spy kullanabiliriz. const handleFitering = spy(); ile bir spy tanımladık ve shallow içerisinde test edeceğimiz bileşene prop olarak geçtik.

enzyme tarafından sağlanan selector methodu ile filtreleme alanındaki elementleri tespit ettik. Bu alan içerisinde üç tane a elementi olması gerektiğini bekliyoruz. Sırasıyla bu üç element üzerinde click simülasyonu yapalım. shallow ve mount ile wrap ettiğimiz bileşenlere enzyme çeşitli methodlar ekleyerek testlerimizi kolaylaştırıyor. Bunlardan biri de simulate. Simulate, bileşenler üzerinde event simülasyonu yapmamızı sağlıyor. Bütün a elementleri üzerinde click simülasyonu yaptık ve sonuçları spy referansı üzerinden test ettik.

Yazı beklediğimden fazla uzadığı için diğer testleri yazmıyorum. Zaten benzer şeyler yapacağız. :) O yüzden diğer testlere GitHub üzerinden erişebilirsiniz. Başlıkta belirttiğimiz, son konumuza geçebiliriz: Test Coverage.

Test coverage yazdığımız testlerin, çalıştırdığımız kodların ne kadarını test ettiğini ifade ediyor. Kodlarımız içindeki satırlardan hangileri çalıştırıldı, koşul ifadelerinde hangi bloklar çalıştı, hangi fonksiyonlar hiç çağırılmadı gibi bilgileri veriyor. Bu işlem için JavaScript’te Istanbul isimli bir kütüphane var ve bu kütüphanenin mocha ile entegre olabilen nyc isimli CLI aracı mevcut. nyc paketini projeye ekleyelim. Bir de, NODE_ENV gibi ortam değişkenleri platform bağımsız yönetmemizi sağlayacak olan cross-env kütüphanesini ekleyelim ve açıklamaya çalışalım.

npm install nyc cross-env --save-dev

nyc için herhangi bir kodlama yapmamıza gerek yok. mocha ile testleri çalıştırırken yanına nyc scriptlerini de eklememiz yeterli. O halde artık package.json içerisine test scriptlerimizi ekleyebiliriz.

İlk scriptimiz npm run test veya npm test ile çalışabilecek ve mocha ile yazdığımız testleri çalıştıracak script. test:coverage ise mocha ile nyc entegrasyonunu yapan ve test coverage sonuçlarını console’a yansıtan script. test:coverage:html ise coverage sonuçlarını HTML formatında raporlayan scriptimiz. Istanbul tarafından sağlanan alternatif rapor seçenekleri de incelenebilir. :)

Test coverage raporunda dört bölüm bulunuyor. Statements Branches Functions Lines Bu bölümler isimlerinden anlaşılacağı üzere, çalıştırılan testlerde tüm satırlar çalıştırıldı mı, bütün koşullu bloklar test edildi mi, bütün fonksiyonlar çağırıldı mı gibi sonuçları açıklıyor. npm run test:coverage:html ile testleri çalıştırıp, proje dizininde oluşan coverage klasöründen çıktılarını inceleyebiliriz.

Yazı biraz fazla uzadığı için son kısımları kısa tutmak durumunda kaldım. Umarım faydalı olmuştur. Net olmayan bölümler olursa, yorum kısmında belirtirseniz düzeltmeye çalışırım. Bu bölümün kodlarına GitHub üzerinden erişebilirsiniz.

--

--