React Uygulamalarında TDD
Test güdümlü React uygulamaları geliştirme ipuçları
Frontend uygulamalarında test-driven development çok yaygın uygulanan bir geliştirme metodolojisi değil. Ancak modern frontend workflow’ları bir noktada TDD’nin önemini tekrar ortaya çıkardı. Modern frontend mimarisi aslında uygulamanın önemli bir kısmının sorumluluğunu frontend’e bırakmayı ön görüyor. Bu noktada test yazmak opsiyonel olmaktan çıkıp standart hale geldi.
Aslında bu yeni bakış açısı beraberinde hız, performans ve stabilite getirdi diyebiliriz. Daha temiz , test edilebilir ve bakımı kolay modüler kod yazılmasını aşıladı bizlere. Bu noktada artan sorumlulukların bir gereği olarak test yazmanın neden gerekli olduğu daha belirgin hale gelmeye başladı.
Frontend dünyasında TDD neden gerekli konusunu ele alıp, modern bir frontend web uygulamasında hangi test türlerine ihtiyaç var ve bu test türleri hangi konuları kapsıyor nasıl yapılıyor aktarmaya çalışacağım. Benim geliştirdiğim modern frontend uygulamalarının temeli React etrafında konumlandığı için odağı buraya yoğunlaştırıp daha çok React üzerinden anlatacağım. Kullanılan framework’lara göre test yazım şekli ve araçlar kısmen de olsa değişebiliyor.
🚦Unit Test ≠ TDD!
TDD konusu geçtiğinde genelde şu sözü duyarız “Unit Test yazıyoruz”. Unit Test her ne kadar TDD’nin temelini oluştursa da TDD’nin tamamını oluşturmamaktadır. Yani sadece Unit Test yazarak TDD yapmış olmazsınız.
Frontend ve backend sadece terminoloji ve teknoloji olarak değil gerçek manada birbirinden ayırılmaya başlandı. Bunun en büyük nedeni maintenance problemleri ve monolithic uygulamaların ilerleyen süreçlerde ciddi sıkıntılar çıkarmasıydı.
Modern frontend mimarileri işte bu problemleri ortadan kaldırmayı amaçlayan yöntem ve teknolojiler sunmaya çalışıyor.
JavaScript bildiğiniz gibi son yıllarda evrimleşmeye ve gerçek bir dil olmaya oldukça yaklaşmış durumda. Modüler bir geliştirme ortamı ile test yazarak ilerlemek artan mantıksal operasyonu yönetme konusunda biraz daha güven verebiliyor biz geliştiricilere.
Neden Test Edilebilir Kodlar Yazmalıyız?
Test-Driven geliştirmenin temel kurallarından bir tanesi test edilebilir kod yazmaktır. Test-First ilerlediğinizde yazdığınız kod tamamen test edilebilir bir koda dönüşüyor. Ancak gerçek hayatta test-first ilerlemek çok gerçekçi bir senaryo olmayabiliyor. O nedenle ileride test yazmak istediğinizde kodunuzda refactor yapmanız gerekiyor. Testin önemini kavramaya başladıktan sonra en baştan test edilebilirliği düşünerek kod yazmanın olası zaman kayıplarının önüne geçtiğini anlayabiliyorsunuz.
Neden Modüler Uygulamalar Geliştirmeliyiz?
Modüler uygulamalar test edilebilirliğin daha kolay olduğu uygulamalardır. Parçalar kendi içinde belli bir görevi yerine getirirler ve bu ufak parçaların test edilmesi daha büyük modellere göre oldukça basittir.
Artık tek bir dosyada yazılan JavaScript kodları kısmen tarih oldu. ES6 ile birlikte hayatımıza giren yeni özellikleri kullanarak code base’i modüller halinde birbirinden lego parçaları gibi ayırabiliyoruz. Modüler kod yazmak en temelde kodun bakımını ve anlaşılabilirliğini kolaylaştırıyor ve daha rahat dökümante edebilmemizin önünü açıyor.
React’ın en sevdiğim yanı component modeli. Her bir component’a ait unit test yazabilirsiniz. Her component tek bir klasör içerisinde tek bir dosyada yazılır, siz bu klasör içerisinde unit testinize ait bir dosya daha tutabilirsiniz. O halde stil dosyamıda burada tutabilirim diyebilirsiniz. Component’a ait asset’leride burada saklarsanız gerçek manada bağımsız bir modül yaratmış olursunuz.
Modüler uygulamalar daha az karışık ve istendiğinde bir parçanın çıkarılıp yerine başka bir parçanın kolaylıkla takılabilmesini amaçlar. Aslında yapmak istediğimiz tam olarak bu. Modüle ait herşeyin aynı yerde hiyerarşik bir düzenle işlenmesi…
TDD’de neyin test edilip, neyin edilmeyecek olması ince bir çizgidir. Her kodun test edilmesi çok gerçekçi değil. Diğer bir yandan code-coverage’ın yüksek olması da aldatmamalı sizi. TDD ile hata ayıklama konusunda işlerin daha kolaylaştığına tanık olabilirsiniz, en güzeli de kod yazmayı daha temiz ve eğlenceli hale getirdiğini göreceksiniz.
Ben TDD konusuna React uygulamaları özelinde bakacağım. Öncelikle hangi konularda test ihtiyacımız var bunu belirleyelim. Ardından örnekler ile açıklamaya çalışalım.
Test piramidinin en altından başlarsak, unit(birim) testler test yazmak için ilk başlangıç noktamızı oluşturuyor.
Ardından birbirinden bağımsız birimlerin bir araya gelmesi ile oluşan yapıların düzgün bir şekilde çalışıp çalışmadığını kontrol etmek için Integration test’ler ile devam ediyoruz.
Piramidin bir üstünde ise E2E testleri görüyoruz. Bunlar genellikle automated ve UI üzerinden gerçek bir kullancı gibi davranarak tüm durumları kontrol etmek için faydalandığımız test türleri.
Piramidin en tepesinde manuel test var. Bunun nedenini yazının sonlarına doğru açıklamaya çalıştım.
Unit Test
Unit test’ler adından da anlaşılacağı gibi birim testler olarak ifade edilebilir. Unit test oldukça önemli bir konu. TDD’nin temelini oluşturmaktadır.
React özelinde düşündüğümüzde her component için bir unit test yazılmalıdır. Component’ın içeriye aldığı prop’ları test edebilirsiniz, css class’larını test edebilirsiniz, beklediğiniz değerin gelip gelmediğini veya beklediğiniz türde mi geldiğini kontrol edebilirsiniz. Component’ın mount olup olmadığını test edebilirsiniz.
Kısacası component’ın yaşam döngüsünde doğru çalışmasını beklediğiniz her işlemi test edebilirsiniz. Böylece component’da bir problem yaşandığında sorunun nereden kaynaklandığını yazacağınız test senaryoları sayesinde önceden tespit edebilirsiniz.
Genellikle test süreçlerimizi build process’lerimize dahil ederiz. Yeni bir geliştirme yaptığımızda önce testlerimiz çalışır ardından ilgili kod parçası uygulama ile birleştirilmeye hazır hale gelir. Test burada hayat kurtarır ve herşeyin yolunda olup olmadığı konusunda fikir sahibi olmanızı sağlar.
React component’larını test etmek için en popüler iki test framework’ünden bahsetmek istiyorum.
JEST
Jest Facebook tarafından sunuluyor. Güzel yanı ise React desteği.
React component’larının test edilebilmesi için render methodunun simule edilmesi gerekiyor.
Jest ile birlikte kullanılan react-test-renderer isimli bir modülü mevcut. Bu modülün bir create.renderer methodu var. İçerisine render ettirmek istediğiniz component’ı veriyosunuz. Sizin için render ediyor ve bu sayede component’ın prop’larına, içerisindeki child’lara erişip klasik test case’lerimizi yazabiliyoruz. Aslında herşeyi bir obje içinde dönüyor biz bu objenin elemanlarına ulaşıyoruz temelde.
Jest kullanmadan önce Enzyme ile shallowRendering yapıyordum. Enzyme Jest ile birliktede kullanılabilen sadece React için özelleştirilmiş bir araç aslında. Bazı durumlarda Enzyme ile birlikte Jest kullanabilirsiniz.
Enzyme
AirBnB tarafından geliştirildi. Kullanımı oldukça basit. Tamamen react odaklı bir araç. Enzmye ile test yazmak JQuery yazmaya çok benziyor. Basit bir syntax’ı var. Eğer Mocha ve Chai kullanmak istiyorum ancak React component’larınıda test etmek istiyorum derseniz Enzyme kullanabilirsiniz.
Ancak bazı case’lerde örneğin bir network activity’i test etmek istediğinizde mesela Ajax call’larını test etmek için Sinon kullanmanız gerekiyor. Sinon ile fake http request’ler yaratabiliriz hatta fake server yaratıp daha farklı senaryolar ile test case’leri yaratabiliriz.
React Component’larına Unit Test Yazmak
React bilindiği gibi farklı bir render mekanizmasına sahip. Component’ları test edebilmek için bir şekilde bu render mekanizmasını test sırasında çalıştırmak gerekiyor. Öncelikle test için kullanacağımız araçlardan bahsedelim, ardından örnek ile bir component test sürecini inceleyelim.
Ben bu örnekte biraz uzun yoldan giderek Enzyme ile birlikte Mocha ve Chai kullanacağım. Ancak bunun daha pratik yolunu Jest ile örneklendireceğim.
Örnek Senaryo:
Test etmek için bir menü yapısı düşünelim. Menüye ait elemanları bir rest servis ile veya farklı bir şekilde dışarıdan almayı planlayalım. Component’ımızı datanın kendisinden haberi olmayacak şekilde tasarlayacağız. Sadece kendisine pass edilecek field’ları ve data yapısını biliyor olacak(presentational component). Buna göre component geliştirelim.
Component Yapısı:
Tasarladığımız component basitçe kendisine pass edilen array tipindeki menü listesini bir ul içerisine li > link > {name} formatında map ederek render ediyor ve kendi stilini veriyor. State tutmadığı için Stateless Component yapısında yazmayı tercih ettim. Yazı daha çok react geliştiricilerine hitap ettiği için kod detayına girmeyeceğim.
Test:
Test kodumuz yukarıdaki yapıya sahip component’a istediği tipte prop’ları pass edecek ve ardından enzyme ile shallow rendering yaparak test case’lerimizi yazacağız. Syntax olarak hemen hemen tüm araçlar için aynı prensipler buradada geçerli.
Püf nokta render edilen elemanları find ile bulmak ve beklediğimiz case’e uygun mu değil mi kontrol etmek. shallow() bize ShallowWrapper isminde bir obje döndürüyor. Tüm olay tam olarak burada gerçekleşiyor. Artık find ile bu obje içerisinde test etmek istediğimiz node’ları yakalayıp expect’den geçirebiliriz. Burdan sonrası için api dökümanını referans almanız gerekiyor.
Class isimleri ile Component içindeki Elemanlara Erişmek:
Yukarıdaki test örneğinde React Router’ın Link’ini find ile yakaladık. Ancak bu her zaman istediğiniz bir kullanım şekli olmayabilir. Bu nedenle style class isimlerinden elementleri yakalamak isteyebiliriz. Bunun için aşağıdaki gibi pratik bir çözüm uygulayabilirsiniz.
Integration Test
Adından aslında ne tür bir test metodolojisi olduğu anlaşılabiliyor. Uygulamadaki bütün parçaların birbiri ile uyum içerisinde çalışıp çalışmadığını anlayabilmek için integration test’ler yazıyoruz. Unit test’lere çok benziyor ancak tamamen aynı değil. Unit test’lerde diğer bileşenlerden izole bir şekilde test yaparken, integration testler izole edilmeden yazılıyor. Yani uygulamanın farklı katmanlarına erişerek birlikte çalışabilirliği test etmeyi amaçlıyor.
Yukarıda gördüğünüz gif integration testi en iyi anlatan gif’lerden biri bana göre. 😊
Sonuç olarak birbirine entegre olan tüm katmanlarda integration testlerden faydalanmalıyız. Unit testlerin yetersiz kaldığı durumlarda oldukça hayat kurtarıcı olacaktır. Temelde integration testlerin unit testlerden daha fazla yapılması gerektiği gibi bir öngörü hakim. Her unit test kendi içinde düzgün çalışabilir ancak bir arada düzgün çalışamadıktan sonra uygulamanın sürdürülebilirliği sağlanamamış olur.
UI Test
UI testleri aslında en keyifli test türü diyebilirim. Bir uygulamayı tamamladıktan sonra bazen istemsiz hatalar yaparak değiştirmememiz gereken bir şeyi değiştirerek tasarım bütünlüğünü ve markup’ı bozabiliriz. İşte bu hataların önüne geçebilmek için UI testleri yazmamız gerekiyor. Başlık alanında h1 tag’i kullanılması uygulama süresince istediğim bir şey ise eğer bu değişirse testin fail olmasını beklerim. Bu doğrultuda yaptığımız şey component içerisinde başlık alanına ait node h1 mi? değil mi? kontrolü yapmak. Bir süre sonra bu çok sıkıcı bir hal almaya başlıyor ve sürekli aynı şeyleri tekrarladığınızı hissediyorsunuz.
Bunun yerine ben Jest’in snapshot test’ini kullanmaya başladım.
SnapShot Test
Jest SnapShot testlerinde birebir react component’larınızın ekran görüntüsünü alıyor bir anlamda. Ancak bu görüntü bir jpg, png gibi ekran görüntüsü değil elbette. Component’ı JSON formatına dönüştürüyor. Daha sonra bunu .snap adında dosyalara yazıyor ve dosya adına -test.snap ekliyor. Ardından toMatchSnapshot() ile sizin Component’ınızda yapılan her değişikliği match etmeye çalışıyor. Aradaki diff ile test’i fail ediyor eğer bir fark yoksa testiniz geçiyor.
UI Testi için Jest kullanıyoruz. Yukarıda açıkladığım gibi Enzyme yada herhangi bir test aracı ile component içindeki elementlere erişerek className kontrolü yapmak kendini tekrarlayan bir test türü olduğu için bu işi Jest’in SnapShot testine bırakıyoruz.
SnapShot test ile sadece tekrar tekrar oluşturduğumuz UI’ın son durumunu kontrol eden case’leri yazmaktan kurtuluyoruz. Data’nın formatındaki doğruluğu veya diğer olasılıkları ilgili bileşenler için kendimiz yazmalıyız kısacası testlerimiz deterministik olmalı.
SnapShot Test Örneği:
SnapShot Çıktısı:
Component’ı Update Edelim:
Test Çıktısı:
Component’da update yapıldıktan sonra jest daha önce aldığı görüntü ile yeni görüntüyü karşılaştırdı ve aradaki diff dolayısı ile testi fail etti. Oldukça kullanışlı ve pratik bir yöntem. Release çıkarken yanlışlıkla UI’ın bozulmasının önüne geçmek içinde kullanılabilir.
Tek dezavantajı snapshot update yönteminin manuel olması. Bunu manuel yapmalarının en önemli sebebi, geliştirici olarak UI’da bir değişiklik yapmıyorsam testlerin update olmaması gerekiyor. Eğer bir değişiklik yaptıysam bunu bildiğim için manuel olarak update yaptırmam çokta mantıksız değil. Aynı zamanda code review sırasında da ekstra bir review maliyetini azaltıyor PR öncesi build sürecinde otomatik olarak check ettirileceği için.
Stillendirme için Styled-Components kullanıyorsanız ui testlerini nasıl yapacağınızı merak ediyor olabilirsiniz. Jest için geliştirilmiş bir utility mevcut.
```yarn add — dev jest-styled-components```
End to End
İşlevselliğin test edilmesi, herşeyin düzgün çalışıp çalışmadığının test edilmesi olarak özetlenebilir. Adındanda anlaşıldığı gibi uçtan uca test yapmayı gerektiriyor. Functional test olarakta isimlendirenler var. Aslında hepsi aynı şeyi ifade ediyor.
Bu testler eskiden manuel yapılıyordu. Test uzmanı bir çok durumu kontrol eden uygulamaya özel check list hazırlar ve bu liste üzerinden kontrollerini manuel yapardı. Bu senaryo zaman açısından maliyetli olduğu için otomasyon sistemleri kullanarak tüm bu check listi otomatik bir şekilde uygulayabiliyoruz.
Otomatik testler, manuel testlere göre daha az maliyetli bir işlemdir. Birebir kullanıcı etkileşimini simule ettiğimiz için yavaş bir test türüdür. Ek olarak UI’da yapılan değişikliklere paralel olarak testlerin update edilmesi gerekebilir. Yani bakımı maliyetli olabiliyor.
Bu nedenle çok detaylı E2E testler yazmaktan kaçınmalıyız. Ortak kullanıcı etkileşimlerini test etmemiz yeterli olacaktır.
Peki hangi aşamada E2E yazmalıyız ?
Örneğin bir üye kayıt sürecini E2E kapsamına rahatlıkla sokabilirsiniz. Mesela kayıt sırasındaki validasyon hatalarının kontrolü, kayıt sonrası success ekranına yönlenip yönlenmediği gibi case’ler ile tüm kayıt sürecini otomatize edebilirsiniz. Manuel olarak yaptığımız testler her defasında aynı check list’e göre hareket etsek dahi insan faktörü dolayısı ile gözden kaçabilir. Ancak bu check list’i selenium ile E2E test’e çevirirseniz hata olasılığı daha çok azalacaktır.
Çok basit detaylı olmayan ve kısmen bazı adımları eksik bir E2E test senaryosu:
- Sayfayı aç
- Üye ol linkine tıkla
- Üye ol formu açılıyor mu?
- Üye ol formunu doldur
- Üye ol butonuna tıkla
- Kayıt başarılı mesajı alınıyor mu?
- Form başarılı mesajı sonrası sayfa istediğim route’a redirect oluyor mu?
- Formu eksik doldur
- Kayıt başarısız mesajı alınıyor mu?
Selenium ile Automated Testler Yazmak
Selenium ile kullanıcı hareketlerine uygun otomatik çalışan testler yazabiliriz. Browser üzerinden önceden belirlenen kurallara uygun olarak çalışan test uygulamaları geliştirilebilir. Özellikle BrowserStack ile entegre olan otomasyonlar gerçekten büyük bir yükü omuzlarımızdan alıyor. Selenium bir çok programlama dili ile yazılabiliyor. Java, Ruby, Python, Node gibi. Hangi dile daha hakimseniz o dille testlerinizi yazabilirsiniz.
Selenium IDE, webdriver ve grid olarak 3 ayrı proje ile birlikte yürütülüyor. Çok detayına girmeden kısaca bahsetmek gerekirse;
- Ide: tarayıcı üzerinde çalışan bir plugin vasıtasıyla kolayca test case’lerin yazılması.
- Webdriver: Biraz daha komplike işler yapmayı sağlayan ve tarayıcıya özel methodları kullanarak test yazmayı amaçlayan Ide’nin aksine geliştirme ortamına ihtiyaç duyan bir yenilik.
- Grid: Dağıtık test otomasyonu geliştirmeyi amaçlar. İşletim sistemi ve donanım özelliklerine göre farklı browser’lar üzerinde testleri paralel olarak çalıştırmaya ve sonuçları pratik bir şekilde görmeyi sağlar.
Biz webdriver üzerinden gideceğiz şimdilik.
Navigate:
Basitçe webdriver kullanarak firefox tarayıcısı üzerinde http://www.google.com domain’ini açtıralım.
Örnek Test Case:
Artık yapacağımız işlemlerin en önemli bölümünü elementlerin kontrolü kapsıyor. Webdriver’da tüm async işlemler promise döndürür.
Google.com adresine gidelim, sayfanın title’ını alıp beklediğimiz title ile aynı mı değil mi kontrol edelim. Bu test örneğinde ben mocha ve chai kullandım.
Sonuç:
Webdriver ile test yazmak oldukça basit. Dökümantasyonu inceleyerek daha kapsamlı testler yazmaya başlayabilirsiniz. Örneğin bir input’a spesifik bir attribute üzerinden örneğin “name” erişerek value gönderebilir daha sonra bu input’un değerini gönderen butona yine aynı şekilde erişip click yaptırabilir ve daha sonra sonucunun beklentilerinizle uyuşuş uyuşmadığını test edebilirsiniz.
Manuel Test
Her ne kadar testleri otomatik hale getirsekte en son insan gözünün onayına ihtiyaç duyuyoruz. Otomasyonların bazen yakalayamadığı durumları QA Engineer’lar yakalar.😎 Kendini tekrarlayan ve ekstra bir göze ihtiyaç duyulmayan durumlarda test otomasyonlarına tanımlayacağımız senaryoları çalıştırabiliriz.
—