Birim Testleri Kılavuzu

İçindekiler
- Giriş
- Birim Testi Temelleri
- Birim Testi Çerçeveleri ve Araçları
- Birim Testinde Uygulanan İyi Pratikler
- Birim Testi Yazımında Kötü Pratikler (Yapılmaması Gerekenler)
1. Giriş
Birim Testinin Amacı
Birim testlerinin amacı yazılım projesinin sürdürülebilir bir şekilde büyümesini sağlamaktır. Bunun için sadece test yazmak yeterli değildir. Yazılan testlerin aynı zamanda kaliteli olması gerekmektedir. Kötü yazılmış testler, yazılımın kalitesini yükseltmez. Bu nedenle birim testi, genellikle sıkı bağlantıdan kaynaklanan test zorluklarını ortaya çıkararak düşük kaliteli kodun belirlenmesinde yardımcı olur.
Projede, sürekli temizlik ve yeniden düzenleme gibi uygun bakım periyotları olmazsa sistem giderek daha karmaşık ve düzensiz hale gelecektir. Bir hatayı düzeltmek daha fazla hataya neden olacak ve yazılımın bir bölümünü değiştirmek diğer yerleri de bozacaktır; bu şekilde bir domino etkisi oluşturarak, kod tabanını güvenilmez hale getirecek ve kodu tekrar istikrara kavuşturmak zor olacaktır.
Birim testleri, yazılım projesine yeni özellikler eklendikten veya kodu yeni gereksinimlere göre daha iyi uyum sağlayacak şekilde yeniden düzenledikten sonra bile mevcut işlevselliğin çalıştığından emin olmanıza yardımcı olacaktır.

Şekil 1. Testlerin Yazılım Geliştirme Sürecine Etkisi
Test Kapsamı ve Hedefleri
Test kapsamının doğru belirlenmesi, yazılımın kritik alanlarının test edilmesini sağlar ve böylece yazılım kalitesi artırılır. Test kapsamı, yalnızca hangi bileşenlerin test edileceğini değil, aynı zamanda testlerin yazılımın genel başarısına olan katkısını da belirler. Bu kapsamda, kapsamlı bir test stratejisi oluşturmak, yazılımın tüm önemli alanlarını güvence altına alır ve eksikliklerin minimize edilmesine yardımcı olur. Test hedefleri ise, belirlenen kapsamın ötesine geçerek, yazılımın kalitesini, güvenilirliğini ve kullanılabilirliğini artırmak için gereken kriterleri netleştirir. Bu hedefler doğrultusunda yapılan testler, yazılımın genel performansını ve kullanıcı deneyimini önemli ölçüde iyileştirebilir.
Test Kapsamı
Test kapsamı, bir test projesinde test edilecek özelliklerin, modüllerin veya işlevlerin belirlenmesi anlamına gelir. Bu, testin kapsamının ne kadar geniş veya dar olacağını ve hangi bölümlerin test edileceğini netleştirmeyi içerir. Test sürecinin sınırlarını çizmek, bu temel adımlar arasında yer alır. Örneğin, bir web uygulamasının test kapsamı kullanıcı girişi, ödeme işlemleri ve arama işlevselliği gibi belirli özellikleri içerebilir. Bu şekilde belirlenen kapsam, testlerin amacına uygun olarak yönlendirilmesini ve yazılımın önemli alanlarının etkili bir şekilde test edilmesini sağlar.
Kapsama metrikleri, bir test setinin ne kadar kaynak kodunu çalıştırdığını gösterir ve genellikle bir test setinin kalitesini değerlendirmek için kullanılır. Ancak bu metriklerin yüksek olması, test setinin kaliteli olduğu anlamına gelmez.
Bu bölümde, en popüler iki kapsama metriği olan kod kapsamı (code coverage) ve dal kapsamından (branch coverage) bahsedeceğiz.
Kod kapsamı: Test seti tarafından çalıştırılan kod satırlarının sayısı ile üretim kod tabanındaki toplam satır sayısı arasındaki oranı gösterir.

1
2
3
4
5
6
7
8
9
10
public static bool isStringLong(string input){
if(input.Length>5)1
return true;2
return false;1
}1
public void test(){
bool result=IsStringLong("abc");
Assert.Equal(false,result);
}
Testin Kısmen Kapsadığı Bir Örnek
Örnek kodu inceleyecek olursak:
-
1
: Test tarafından kapsamaya dâhildir. -
2
: Test tarafından kapsamaya dâhil değildir.
Yöntemdeki toplam satır sayısı beştir. Test tarafından yürütülen satır sayısı dörttür. Test, return true
ifadesi
dışındaki tüm kod satırlarından geçer.
Bu bize 4/5 = 0,8 = %80
oranında kod kapsamı sağlar.
Dal kapsamı: Kod kapsamı metriğinin eksikliklerini gidermeye yardımcı olan bir diğer metriktir. Bu metrik, kod
satırlarının ham sayısı yerine kontrol yapılarına, örneğin if
ve switch
ifadelerine odaklanır. Metrik, kontrol
yapılarının kaç tanesinin en az bir test tarafından gezildiğini gösterir.

1
2
3
4
5
6
7
8
9
10
public class myClass {
public static bool IsStringLong(string input) {
return input.Length > 5 ? true : false;
}
public void test() {
bool result = IsStringLong("abc");
Assert.Equal(false, result);
}
}
Testin Kısmen Kapsadığı Bir Örnek
Örnek kodu inceleyecek olursak:
isStringLong
yönteminde iki dal vardır:
- Biri, bağımsız değişkenin uzunluğunun beş karakterden büyük olduğu durum için
- Diğeri ise, uzunluğunun beş karakterden büyük olmadığı durum için
Test, bu dallardan yalnızca birini kapsadığından, dal kapsamı oranı 1/2 = 0,5 = %50
’dir.
Test Hedefleri
Testlerin kapsamı belirlendikten sonra, hedeflerin net bir şekilde tanımlanması kritik öneme sahiptir. Test hedefleri, kapsamın ötesine geçerek, yazılımın kalitesini, güvenilirliğini ve kullanılabilirliğini artırmak için belirli kriterlere ulaşmayı amaçlar. Bu hedefler doğrultusunda, testlerin hem değerini hem de bakım maliyetlerini dikkatlice yönetmek gerekir. Test hedefleri aşağıdaki temel unsurları içerir:
- Kalite Kontrol: Yazılımın genel kalitesini artırmak için hataları tespit etmek ve düzeltmek.
- Güvenilirlik: Yazılımın istikrarlı ve güvenilir bir şekilde çalışmasını sağlamak.
- Kullanılabilirlik: Yazılımın kullanıcı dostu olmasını sağlamak.
Projeye daha fazla test yazarak birim testi hedefine ulaşılmaz. Hem testin değerini hem de bakım maliyetini göz önünde bulundurmak gerekir. Maliyet bileşeni, çeşitli faaliyetlere harcanan süreye göre belirlenir. Bu faaliyetlerden bazıları şöyledir:
- Temel kodu yeniden düzenlediğinizde testi yeniden düzenleme.
- Testi her kod değişikliğinde çalıştırma.
- Testin ortaya çıkardığı yanlış alarmlarla başa çıkma.
- Temel kodun nasıl davrandığını anlamaya çalışırken testi okumaya zaman harcamaktır.
Bu kapsamda;
- Bir test kod kapsama yüzdesini hedef olarak belirlemek yanıltıcı olabilir. Kod kapsama metriği bir hedeften ziyade bir gösterge olarak düşünülmelidir.
- Düşük kod kapsama yüzdesi, kod tabanında test edilmemiş çok sayıda kod olduğunu gösterirken, yüksek oranlar yüksek kaliteli bir test kümesi olduğu anlamına gelmemelidir.
- Başarılı bir test kümesi; geliştirme döngüsüne entegre edilmeli, kod tabanının en önemli kısımlarını hedef almalı ve minimum bakım maliyetiyle maksimum değer sağlanmalıdır.
- Tüm testler eşit değildir. Her testin bir maliyeti ve bir fayda bileşeni vardır. Bu iki bileşeni dikkatlice tartmak gerekir.
- Bir kodun birim test yazımına uygun olup olmadığını kontrol etmek, kodun kalitesi hakkında negatif bir gösterge olarak işe yarar ama pozitif bir gösterge olarak işe yaramaz.
- Birim testi hedeflerine ulaşmak için iyi ve kötü testler arasında ayrım yapmak, ayrıca daha fazla değer elde etmek amacıyla testleri yeniden düzenlemek gerekir.
Projeye daha fazla test yazarak birim testi hedefine ulaşılmaz. Hem testin değerini hem de bakım maliyetini göz önünde bulundurmak gerekir.
2. BİRİM TESTİ TEMELLERİ
Birim Testi Nedir?
Birim testi, yazılım geliştirme sürecinde en küçük test birimi olan birimlerin (genellikle fonksiyonlar, metotlar veya sınıflar gibi) doğru çalışıp çalışmadığını değerlendirmek için kullanılan otomatik veya manuel testlerdir.
Birim testleri, tek bir davranış veya kod birimini inceleyen hızlı ve izole testlerdir. Bu testler, birimin beklenen şekilde çalıştığından emin olmak amacıyla yazılır. Birim testi, uygulamanın küçük bir parçasını, geri kalanından bağımsız bir şekilde çalıştırarak bu parçanın davranışını doğrulayan bir metottur.
Birim testi metodu genellikle üç aşamadan oluşur; bu, AAA (Arrange-Act-Assert) Modeli olarak geçer:
- Arrange (Hazırla): Test edilecek koda verilecek olan girdi parametrelerinin belirlendiği ve test edilecek kodun bağımlı olduğu diğer bileşenlerin durumlarının tanımlandığı kısımdır.
- Act (Eylem): Test edilecek kodun çalıştırıldığı aşamadır. Bu aşamada test edilecek fonksiyonu/metodu tetikleriz.
- Assert (Doğrula): Test sonuçlarının doğrulanması aşamasıdır. Tetiklenen fonksiyon doğru sonucu üretiyor mu veya bağımlı olduğu bileşenler üzerinde beklenen aksiyonları tetikliyor mu kontrolünü bu aşamada yaparız.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class myClass {
@Test
void findByNameExactMatchWithTwoPersonsReturnsBothOfThemInInsertionOrder() {
// Arrange
final var phoneBook = new PhoneBook();
phoneBook.addPerson(new Person("Ahmet", "Ankara"));
phoneBook.addPerson(new Person("Mehmet", "İstanbul"));
phoneBook.addPerson(new Person("Mehmet", "İzmir"));
// Act
final var result = adressBook.findByName("Mehmet");
// Assert
assertEquals(2, result.size());
assertEquals(new Person("Mehmet", "İstanbul"), result.get(0));
assertEquals(new Person("Mehmet", "İzmir"), result.get(1));
}
}
Örnek
İzolasyon konusunda iki farklı ekol vardır: Klasik ve Londra yaklaşımı. Bu farklı görüşler, birim testinde “birim” olarak neyin kabul edildiğini ve bağımlılıkların nasıl ele alındığını belirler.
Birim Testi’ndeki ‘Birim’ Kavramı:
-
Klasik Okul: Birim testlerinin birbirinden izole edilmesi gerektiğini belirtir ve test edilen birimin kodun bir parçası değil, bir davranış birimi olması gerektiğini savunur. Birim testlerinin birbirinden izole bir şekilde çalıştırılması gerekir. Bu, testlerin birbirlerinin sonuçlarını etkileyebileceği paylaşılan bağımlılıklara erişmemesi anlamına gelir. Klasik birim testinde, sınıflar ve nesneler gibi gerçek işbirlikçilerinin yerini test dublörleri (mock ve stub) almaz. Bunun yerine, test sırasında gerçek bağımlılıklar kullanılır.
-
Londra Okulu: Birimlerin birbirinden izole edilmesi gerektiğini belirtir. Bir birim genellikle bir sınıftır ve tüm bağımlılıklarının testlerde test çiftleriyle değiştirilmesi gerektiğini savunur. Bu yaklaşım, daha büyük sınıf ağlarını test etme kolaylığı ve bir testin başarısız olması durumunda hangi fonksiyonelliğin başarısız olduğunu kesin olarak bilmek gibi avantajlar sunar. Londra okulunda odak noktası, test edilen birimin işbirlikçilerinden izole edilmesidir. Bu, tam izolasyonu elde etmek için tüm bağımlılıkların (değişmez olanlar hariç) test dublörleriyle (mock ve stub) değiştirilmesi anlamına gelir.

Birim Testlerinde Bağımlılık Türleri
-
Test Dublörleri: Gerçek bağımlılıkların basitleştirilmiş halidir. Gerçek bağımlılıklara benzer şekilde davranırlar ve incelenen birimin test edilmesini kolaylaştırırlar.
-
Paylaşılan Bağımlılık: Testler arasında paylaşılan ve testlerin birbirlerinin sonucunu etkilemesine neden olan bir bağımlılıktır.
-
Özel Bağımlılık: Tek bir teste özgü olan ve paylaşılmayan bağımlılıktır.
-
Değişken Bağımlılık: Belirli özelliklere sahip bir bağımlılıktır; örneğin, bir veri tabanı veya API hizmeti ek kurulum gerektirir veya her çağrıda farklı sonuçlar döndüren nondeterministik davranışa sahip olabilir.
-
Süreç Dışı Bağımlılık: Uygulamanın yürütme sürecinin dışında çalışan bir bağımlılıktır. Henüz hafızada olmayan verilere ait bir proxy’dir.
Bağımlılıkların Ele Alınması
- Londra Okulu genellikle değiştirilebilir olmayan bağımlılıklar dışındaki tüm bağımlılıklar için test çiftlerini (mock objeleri gibi) kullanma eğilimindedir.
-
Klasik Okul ise paylaşılan bağımlılıklar için test çiftlerini kullanma eğilimindedir.
-
Test Çifti: Bir yazılımın farklı bileşenlerini (sınıflar, modüller, servisler vb.) izole ederek ve testler sırasında bu bileşenlerin beklenen davranışlarını simüle ederek kullanılır. Bu sayede yazılımın işlevselliği ve doğruluğu test edilebilir.
- Collaborator vs. Dependency: Collaborator, paylaşılan veya değiştirilebilir bir bağımlılıktır. Örneğin, veri tabanına erişim sağlayan bir sınıf, veri tabanı paylaşılan bir bağımlılık olduğundan collaborator’dır.
Londra Yaklaşımının Faydaları
- Geliştirilmiş ayrıntı düzeyi: Testler ayrıntılıdır ve aynı anda yalnızca bir sınıfı kontrol eder.
- Birbirine bağlı sınıfların daha kolay test edilmesi: Tüm ortak çalışanların yerini test dublörleri aldığından, testleri yazarken onlar hakkında endişelenmeye gerek yoktur.
- Kesin hata konumu: Bir test başarısız olduğunda, yalnızca test edilen sınıf dâhil edildiğinden hangi işlevin hatayı içerdiğini belirlemek daha kolaydır.
Klasik vs Londra Okul Arasında Tercih Londra yaklaşımı, aşırı karmaşık ve kırılgan testlere yol açabileceğinden, klasik birim testleri daha sağlam testler üretme eğiliminde olduğundan klasik birim testi ekolü genellikle tercih edilmektedir.
Neden Birim Testleri Kullanmalıyız?
-
Hataların Erken Fark Edilmesi: Birim testi yazılarak geliştirilen bir kodda yapılabilecek hatalar henüz o kodun geliştirme aşamasında fark edilecektir.
-
Kodun Dokümantasyonu: Kaliteli testler, test ettiği kodun bir dokümantasyonudur.
-
Kodun Karmaşıklığını Azaltır, Kalitesini Artırır: Birim testi yazabilmek için en küçük yazılım bileşenlerinin bile tasarımını planlamak gerekir. Karmaşık, kompleks bir kodun testi de o kadar karmaşık ve maliyetli olacaktır. Bu maliyet bizi test edilebilir, ufak sınıflar/fonksiyonlar yazmaya zorlar. Bu şekilde kodun daha okunabilir, daha az kompleks ve sonunda daha kaliteli olmasını sağlar.
-
Hata Bulmayı Kolaylaştırır: Problemli olduğu düşünülen işlemin geçtiği kod bileşenlerine hataya sebep olabilecek test verilerinin girdi olarak verildiği ve çıktıların kontrol edildiği bir birim testi yazılır. Birim testlerinde herhangi bir dış entegrasyon veya bağımlılık olmadığından hatayı bulma süreci daha hızlı ve daha kolay sonuçlanır.
-
Önemli Bir Sürekli Entegrasyon (Continuous Integration) Adımı: Birim testleri olmayan projelerde Sürekli Entegrasyon işlemi sadece projenin derlenebildiğini, derleyici hatası içermediğini garanti edebilir. Birim testleri olan projelerde ise derleme sonrasında test kodu otomatik olarak çalışacağından aynı zamanda var olan fonksiyonelliğin bozulmadığı da anlaşılır.
Test Odaklı Geliştirme İlkeleri
Test Odaklı Geliştirme (TDD), yazılım geliştirmede, gerçek özellik veya işlev yazılmadan önce test vakaları yazmaya vurgu yapılan yinelemeli geliştirme döngüsüne odaklanan bir metodolojidir. TDD, kısa geliştirme döngülerinin tekrarını kullanır ve yazılım geliştirme ile test etmeyi birleştirir. Bu süreç sadece kodun doğruluğunu sağlamaya yardımcı olmakla kalmaz, aynı zamanda eldeki projenin tasarımını ve mimarisini dolaylı olarak geliştirmeye de yardımcı olur.
TDD genellikle “Kırmızı-Yeşil-Refactor” döngüsünü takip eder:
- Test paketine bir test ekleyin.
- (Kırmızı) Yeni testin başarısız olduğundan emin olmak için tüm testleri çalıştırın.
- (Yeşil) Tek testi geçmek için yeterli kod yazın.
- Tüm testleri çalıştırın.
- (Refactor) Testleri yeşil tutarken ilk kodu okunabilir ve bakımı kolay hale getirmek için güvenli bir şekilde iyileştirin.
- Tekrarla.

Şekil 2. Kırmızı-Yeşil-Refactor Şeması
3. Birim Testi Çerçeveleri ve Araçları
Yazılım geliştirme sürecinde birim testleri, kodun doğru çalıştığından emin olmanın önemli bir parçasıdır. Doğru birim testi çerçevesi seçmek, testlerin etkili bir şekilde yazılmasına ve yönetilmesine yardımcı olur. Popüler birim testi çerçevelerinden bazıları ve kullanımları ile alakalı bilgiler alt başlıklarda ele alınmıştır.
Popüler Birim Testi Çerçeveleri
1. JUnit
JUnit, Java dilinde kullanılan en popüler birim testi çerçevelerinden biridir. Java tabanlı projelerde sıkça tercih edilir. JUnit, test driven development’ın geliştirilmesinde önemli olmuştur. SUnit ile başlayan ve toplu olarak xUnit olarak bilinen birim test çerçeveleri ailesinden biridir. JUnit derleme zamanında bir JAR olarak bağlanır. Çerçevenin en son sürümü olan JUnit 5, JVM’de geliştirici tarafı testleri için modern bir temel sağlar. Buna Java 8 ve üstüne odaklanmanın yanı sıra birçok farklı test stilinin etkinleştirilmesi de dâhildir.
1
2
3
4
5
6
7
8
9
10
11
import static org.junit.Assert.*;
import org.junit.Test;
public class MyTest {
@Test
public void testMethod() {
// Test kodu buraya yazılır
assertEquals(expected, actual);
}
}
JUnit Test Şablonu
2. Pytest
Pytest, PyPy projesinden kaynaklanan, python dilinde yaygın olarak kullanılan bir birim testi çerçevesidir. Basit syntax’ı ve geniş dokümantasyonu ile bilinir. Birim testleri, entegrasyon testleri, uçtan uca testler ve işlevsel testler dâhil olmak üzere çeşitli yazılım testleri geliştirmek için kullanılabilir.
1
2
def test_example():
assert 1 + 1 == 2
Pytest Test Şablonu
3. RSpec
Ruby dilinde kullanılan bir birim testi çerçevesidir. Özellikle Ruby on Rails projelerinde sıkça kullanılır. Test tabanlı geliştirme ortamlarında tercih edilen RSpec, komut satırında çalışan ana program (rspec) haricinde örnek test dosyalarını ve açıklamalarını barındıran rspec-core, genişletilebilir beklenti (expectation) dili paketi rspec-expectations, dâhili mock/stub desteği sunan rspec-mocks paketleri ile gelir.
1
2
3
4
5
RSpec.describe "MyClass" do
it "does something" do
expect(subject.method_name).to eq(expected_result)
end
end
RSpec Test Şablonu
4. NUnit
.NET platformu için bir birim testi çerçevesidir. C#, F#, ve Visual Basic gibi dillerle uyumludur. Java dilindeki JUnit ile aynı amaca hizmet eder ve xUnit ailesindeki birçok programdan biridir.
1
2
3
4
5
6
7
8
[TestFixture]
public class MyTests {
[Test]
public void TestMethod() {
// Test kodu buraya yazılır
Assert.AreEqual(expected, actual);
}
}
NUnit Test Şablonu
5. Mocha
JavaScript dilinde kullanılan bir birim testi çerçevesidir. Özellikle Node.js projelerinde ve tarayıcıda çalışan uygulamalarda kullanılır. Mocha, Node.js programları için tarayıcı desteği, eşzamansız test, test kapsamı raporları ve herhangi bir onay kitaplığının kullanımını içerir.
1
2
3
4
5
6
7
const assert = require('assert');
describe('MyFeature', function () {
it('should do something', function () {
assert.equal(1 + 1, 2);
});
});
Mocha Test Şablonu
Araç Seçim Kriterleri
Birim testi çerçevesi seçerken, aşağıdaki kriterler göz önünde bulundurulmalıdır:
-
Proje Uyumluluğu
Projenin programlama diline uygun bir birim testi çerçevesi seçmek önemlidir. Çoğu çerçeve belirli dillerle uyumlu olabilir, ancak bazıları diğerlerinden daha iyi bir entegrasyon sunabilir. -
Topluluk Desteği
Çerçevenin geniş bir topluluk tarafından destekleniyor olması, karşılaşılan sorunlara çözüm bulmayı kolaylaştırabilir. Yaygın olarak kullanılan çerçeveler genellikle daha büyük ve aktif topluluğa sahiptir. -
Dokümantasyon Kalitesi
İyi bir dokümantasyon, çerçevenin nasıl kullanılacağı konusunda rehberlik eder. Kapsamlı ve anlaşılır belgeler, hızlı bir öğrenmeyi ve sorun gidermeyi sağlar. -
Sürekli Entegrasyon (CI) Desteği
Birim testlerini sürekli entegrasyon sürecine entegre etmek, hataların erken aşamada tespit edilmesine yardımcı olabilir. Çerçeve, popüler CI araçlarıyla uyumlu olmalıdır. -
Performans ve Esneklik
Bazı çerçeveler daha hızlı çalışabilir veya daha esnek konfigürasyon seçenekleri sunabilir. Projenin ihtiyaçlarına en iyi uyacak çerçeveyi seçmek önemlidir.
Test Çerçevelerinin Kurulumu ve Yapılandırılması
Her bir birim testi çerçevesi farklı kurulum ve yapılandırma adımlarına sahiptir, ancak genel olarak izlenen temel adımlar şu şekildedir:
- Bağımlılıkların Yüklenmesi
Projeye ilgili birim testi çerçevesini eklemek için genellikle bir bağımlılık yönetim aracı kullanılır. Maven, Gradle, npm, pip gibi araçlarla çerçeve bağımlılıkları projeye eklenir. Örnek olarak JUnit maven projesine aşağıdaki şekilde eklenebilir:
1
2
3
4
5
6
7
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
-
Konfigürasyon Dosyalarının Ayarlanması
Bazı çerçeveler özel yapılandırma dosyalarını destekler. Bu dosyalar aracılığıyla çerçevenin davranışı özelleştirilebilir. Örneğin, bir test veri tabanı konfigürasyonu veya raporlama ayarları gibi. -
Test Kodu Yazma
Çerçeve ile uyumlu olarak test kodunu yazmak. Her bir çerçeve kendi syntax’ına ve kurallarına sahiptir, bu nedenle test çerçevesinin dokümantasyonlarını incelemek önemlidir. -
Testleri Çalıştırma
Proje derlenip çalıştırıldığında, birim testlerinin otomatik olarak çalıştırılması için bir komut veya arayüz kullanılabilir. IDE’ler genellikle bu işlevselliği sağlar, ancak terminal veya CI/CD araçları da kullanılabilir. -
Raporlama ve Analiz
Test çerçeveleri test sonuçlarını raporlar. Bu raporlar, hangi testlerin başarılı olduğunu, hangilerinin başarısız olduğunu ve performans istatistikleri gibi bilgileri içerebilir.
4. Birim Testinde Uygulanan İyi Pratikler
Birim testi, yazılım geliştirme sürecinde önemli bir aşamadır ve iyi bir uygulama, güvenilir ve kaliteli bir ürünün ortaya çıkmasına katkıda bulunabilir. Aşağıda birim testlerinde uygulanan iyi pratiklere dair bazı örnekler verilmiştir:
-
Okunabilir, Anlaşılır Basit Testler Oluşturun: Basit test senaryolarını yazmak, sürdürmek ve anlamak daha kolaydır. Testlerinizde mantık, manuel dizi veya döngüsel karmaşıklık gibi özelliklerin minimum düzeyde olmasına özen gösterin.
- Aşağıdaki örnekte
Calculator
sınıfı, dört temel matematiksel işlemi gerçekleştirebilen basit bir sınıftır. Test sınıfında ise bu sınıfın her bir metodunu test etmek için ayrı test metotları bulunmaktadır. Bu testler, her bir matematiksel işlemi ayrı ayrı test eder ve test senaryolarını anlaşılır bir şekilde ifade eder. Bu sayede her testin neyi test ettiği kolayca anlaşılabilir ve testlerin sürdürülmesi daha basit hale gelir.
- Aşağıdaki örnekte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class CalculatorTest {
@Test
public void testAddition() {
// Arrange
Calculator calculator = new Calculator();
// Act
int result = calculator.add(3, 5);
// Assert
assertEquals(8, result);
}
@Test
public void testSubtraction() {
// Arrange
Calculator calculator = new Calculator();
// Act
int result = calculator.subtract(8, 3);
// Assert
assertEquals(5, result);
}
@Test
public void testMultiplication() {
// Arrange
Calculator calculator = new Calculator();
// Act
int result = calculator.multiply(4, 6);
// Assert
assertEquals(24, result);
}
@Test
public void testDivision() {
// Arrange
Calculator calculator = new Calculator();
// Act
double result = calculator.divide(10, 2);
// Assert
assertEquals(5.0, result, 0.0001);
}
}
Uygun Test
-
Tek Bir Kullanım Senaryosunu Ele Alın: Her birim testi, yalnızca tek bir kullanım durumunu test etmek üzere düzenlenmelidir. Birim testi başına bir senaryonun ele alınması, bir test başarısız olduğunda sorunu içeren belirli bölümlerin izole edilmesine yardımcı olur.
- Aşağıdaki örnekte,
UserManagement
sınıfı bir kullanıcı eklemek içinaddUser
metodunu içerir. Test sınıfındakitestAddUser
metodu, bu metodun doğru çalıştığını doğrulamak için yazılmıştır. Bu test sadece kullanıcı eklemeyi ele alır ve başka bir senaryoyu içermez. Bu, testin başarısız olduğunda hatanın nerede olduğunu daha hızlı tespit etmeye yardımcı olur. Eğer başka senaryoları test etmek istersek, ayrı test metotları oluşturabiliriz. Bu sayede her test metodu sadece belirli bir senaryoyu ele alır ve sorun izole edilmiş bir şekilde çözülebilir.
- Aşağıdaki örnekte,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserManagementTest {
@Test
public void testAddUser() {
// Arrange
UserManagement userManagement = new UserManagement();
User newUser = new User("Batuhan", "batuhan@email.com");
// Act
boolean result = userManagement.addUser(newUser);
// Assert
assertEquals(true, result);
assertEquals(newUser, userManagement.getUserByEmail("batuhan@email.com"));
}
}
Uygun Test
- Testlerinize Uyumlu İsimler Verin: Birim testinde uygulanabilecek standart adlandırma kurallarını uygulayın. Fazla
uzun olmasından kaçının ve test edilen bölümle alakalı isimler kullanın. Uyumlu test adları, hem kodu yazan
programcının hem de gelecekte bu kodla ilgili çalışacak diğer kişilerin kodun daha kolay okunmasına katkıda bulunur.
Testinizin adı üç bölümden oluşmalıdır:
- Test edilen yöntemin adı.
- Test edildiği senaryo.
-
Senaryo çağrıldığında beklenen davranış.
- Aşağıdaki örnekte,
ProductService
sınıfınıncalculateTotalPrice
metodu test ediliyor. Buradaki isimlendirmeler, testin amacını açıkça ifade eder ve testin içeriğinin anlaşılması daha kolay hale gelir.WhenNoDiscountApplied
testi, indirim uygulanmadığında doğru davranışı kontrol ederken,WhenDiscountApplied
testi, indirim uygulandığında doğru davranışı kontrol eder. Bu şekilde, test adları hem kodu yazan kişiye hem de diğer geliştiricilere testin amacını ve beklenen davranışını anlatmış olur.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ProductServiceTest {
@Test
public void calculateTotalPrice_WhenNoDiscountApplied_ShouldReturnOriginalPrice() {
// Arrange
ProductService productService = new ProductService();
Product product = new Product("Laptop", 1500.0);
// Act
double totalPrice = productService.calculateTotalPrice(product);
// Assert
assertEquals(1500.0, totalPrice, 0.001);
}
@Test
public void calculateTotalPrice_WhenDiscountApplied_ShouldReturnDiscountedPrice() {
// Arrange
ProductService productService = new ProductService();
Product discountedProduct = new Product("Headphones", 100.0);
discountedProduct.applyDiscount(0.2); // 20% indirim
// Act
double totalPrice = productService.calculateTotalPrice(discountedProduct);
// Assert
assertEquals(80.0, totalPrice, 0.001);
}
}
Uygun Test
-
Testler Arasındaki Karşılıklı Bağımlılıktan Kaçının: Birim testleri, bireysel kod birimlerini doğrulamayı amaçlar; bu nedenle testlerin birbirine bağımlılığından kaçınmalısınız. Yeterli kod kapsamına ulaşmak için birçok yapılandırılmış testten daha az sayıda, ancak kaliteli birim testleri oluşturmalısınız.
- Aşağıdaki örnekte, iki farklı matematiksel işlemi içeren iki ayrı test metodu bulunmaktadır:
testAddition
vetestSubtraction
. Her iki test de kendi içinde bağımsızdır ve diğerinin sonuçlarına ihtiyaç duymaz. Bu, testlerin birbirine bağımlı olmaması anlamına gelir. Her bir test, bireysel bir kod birimini doğrulamayı amaçlar. Bu sayede, bir testin başarısız olması diğerini etkilemez ve her bir testin belirli bir işlevi doğru bir şekilde gerçekleştirip gerçekleştirmediği ayrı ayrı kontrol edilebilir. Bu, testlerin bağımsız olmasını sağlayarak, kodunuzda yapılan değişikliklerin sadece ilgili testleri etkilemesini sağlar, bu da bakım ve geliştirme süreçlerini daha yönetilebilir hale getirir.
- Aşağıdaki örnekte, iki farklı matematiksel işlemi içeren iki ayrı test metodu bulunmaktadır:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MathOperationsTest {
@Test
public void testAddition() {
// Arrange
MathOperations mathOperations = new MathOperations();
// Act
int result = mathOperations.add(3, 5);
// Assert
assertEquals(8, result);
}
@Test
public void testSubtraction() {
// Arrange
MathOperations mathOperations = new MathOperations();
// Act
int result = mathOperations.subtract(8, 3);
// Assert
assertEquals(5, result);
}
}
Uygun Test
-
Aktif API Çağrılarından Kaçının: Testlere dâhil edilmesi gerekmeyen veri tabanlarının veya diğer hizmetlere yönelik API çağrılarının, testler yürütülürken etkin olmadığından emin olun. API taslaklarını belirli birimlerle sınırlayarak, beklenen davranış ve yanıtları içeren testleri tercih edin.
- Aşağıdaki örnekte,
UserService
sınıfının bir kullanıcı bilgisini almak içinDatabaseConnection
adlı bir bağımlılığı olduğunu varsayalım. Burada gerçek bir veri tabanı çağrısı yapmadan bir yöntemin davranışını doğrulamak için taklit (mock) çerçevelerini kullanarak, bağımlılıkların davranışını taklit eden sahte nesneler oluşturulmalıdır.when
metodunu kullanarak,getUserById
metodunun argümanı 1 ile çağrıldığında beklenen davranışı tanımlarız. Bu şekilde, birim testi, gerçek bir veri tabanına aktif çağrı yapmadanUserService
‘in davranışını test etmeye odaklanır. Mock nesneleri kullanarak bağımlılıkların davranışını kontrol edebilir ve birim testini belirli kod birimlerine izole edebilirsiniz.
- Aşağıdaki örnekte,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserServiceTest {
@Test
public void testGetUserById() {
// Arrange
DatabaseConnection mockDatabase = mock(DatabaseConnection.class);
when(mockDatabase.getUserById(1)).thenReturn(new User(1, "Batuhan"));
UserService userService = new UserService(mockDatabase);
// Act
User user = userService.getUserById(1);
// Assert
verify(mockDatabase, times(1)).getUserById(1);
assertEquals(1, user.getId());
assertEquals("Batuhan", user.getName());
}
}
Uygun Test
-
Testleri Kod İncelemelerine Dâhil Edin: Kod inceleme sürecinde uygulama kodunu ve testleri bir arada değerlendirin. Testlerin kodla birlikte yazılması, sadece planlanan güncellemeler ve değişiklikler için değil, aynı zamanda hata düzeltmeleri için de kritiktir. Her hata düzeltmesini doğrulayan bir test bulundurmak, her hatanın düzeltilmiş olduğundan emin olmanıza yardımcı olacaktır.
-
Birim Testlerini Mümkün Olduğunca Hızlı Olacak Şekilde Tasarlayın: Yavaş testler süreci aksatır ve sıklıkla kullanılamazlar. Hızlı ve etkili bir test süreci, geliştiricilere daha verimli bir çalışma ortamı sağlar ve hataların daha hızlı tespit edilip düzeltilmesine olanak tanır.
-
Hızlı bir test süreci için bazı stratejiler şunları içerebilir:
-
Bağımsız Testler: Her testin diğerinden bağımsız olmasını sağlayın. Bu, testlerin paralel olarak veya belirli bir sırayla çalıştırılmasını mümkün kılar.
-
Mocking ve Stubbing: Gereksiz dış bağımlılıkları engelleyerek test sürecini hızlandırabilirsiniz. Mocking ve stubbing, testlerin daha hızlı çalışmasına olanak tanır.
-
Test Verileri İyi Tanımlanmış Olmalı: Test verileri, testin özel bir durumu hedeflemesi için yeterli olmalıdır. Karmaşık veya büyük veri setleriyle test yapmak, test süresini uzatabilir.
-
Paralel Test Yürütme: Eğer mümkünse, testleri paralel olarak yürüterek süreyi kısaltabilirsiniz.
-
Hafif Test Çerçeveleri Kullanma: Hafif ve hızlı test çerçeveleri seçerek, test sürecini optimize edebilirsiniz.
-
-
-
Test Otomasyonunu Benimseyin: Birim testleri manuel olarak gerçekleştirilebilir, ancak günümüzde mevcut uygulamalar genellikle otomatik bir birim test yaklaşımını teşvik etmektedir. Bu otomatikleştirilmiş birim test süreci, ekiplerin kod kapsamı, test çalıştırma sayısı, değiştirilmiş kod kapsamı ve performans gibi çeşitli önemli ölçümleri tartışabilmesine katkıda bulunur. Otomatik birim testler, kod değişiklikleri yapıldığında veya yeni özellikler eklendiğinde hızlı bir şekilde çalıştırılabilir ve uygulamanın beklenen davranışını doğrulayabilir.
-
Yüksek Test Kapsamını Hedefleyin: Geliştiriciler, bir yazılım uygulamasını eksiksiz ve kapsamlı şekilde test etmeye odaklanmalıdır. Ancak, zaman ve mali sınırlamalar nedeniyle bu her zaman mümkün olmasa da yüksek kapsama ulaşmayı amaçlaması önemlidir.
-
Tekrarlanabilir ve Ölçeklenebilir Birim Testleri Yazın: Test stratejinizin başarılı olması için birim testlerinizi tekrarlanabilir ve ölçeklenebilir hale getirin. Uygulama kodunu yazan herkesin, birim testlerini aynı anda yazmasını sağlamak için düzenli bir uygulama yapısı oluşturun. Davranış veya test odaklı programlama gibi yöntemlerle, uygulama kodunu yazmadan önce testleri dahi oluşturabilirsiniz. Her iki durumda da, testleri uygulama koduna yakın bir şekilde oluşturmak önemlidir. Bunlara ek, testlerin deterministik olması da önemlidir çünkü değişken sonuçlar veren testlere güvenilmez. Testlerinizi yazarken, mümkün olan en iyi şekilde diğer test senaryolarından, çevresel değerlerden ve dış bağımlılıklardan izole etmelisiniz.
- Aşağıdaki örnekte, bir alışveriş sepeti uygulamasının birim testlerini içeren bir sınıf (
ShoppingCartTest
) bulunmaktadır. Her bir test metodu, alışveriş sepetinin farklı davranışlarını kontrol eder:testAddItem
: Bir öğe eklenip eklenmediğini kontrol eder.testRemoveItem
: Bir öğenin doğru bir şekilde kaldırılıp kaldırılmadığını kontrol eder.testCalculateTotalPrice_WithDiscount
: Bir öğe ve bir indirim uygulandığında toplam fiyatın doğru hesaplanıp hesaplanmadığını kontrol eder.testCalculateTotalPrice_WithoutDiscount
: Bir öğe ve indirim olmadığında toplam fiyatın doğru hesaplanıp hesaplanmadığını kontrol eder.- Aşağıdaki test tekrarlanabilir ve ölçeklenebilirdir çünkü:
- Bağımsızlık: Test, diğer testlerden bağımsız olarak çalışabilir.
- Tekrarlanabilirlik: Her seferinde aynı çıkışı üretir.
- Ölçeklenebilirlik: Yeni testler eklenirse veya kod değişiklikleri yapılırsa, bu testlerle birlikte sorunsuz bir şekilde çalışabilir.
- Deterministik: Test, her zaman aynı giriş değerleriyle aynı çıkış sonuçlarını üretir.
- Aşağıdaki örnekte, bir alışveriş sepeti uygulamasının birim testlerini içeren bir sınıf (
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Uygun Test
public class ShoppingCartTest {
@Test
public void testAddItem() {
// Arrange
ShoppingCart cart = new ShoppingCart();
// Act
cart.addItem();
// Assert
assertEquals(1, cart.getItemCount());
}
@Test
public void testRemoveItem() {
// Arrange
ShoppingCart cart = new ShoppingCart();
// Act
cart.addItem();
cart.removeItem();
// Assert
assertEquals(0, cart.getItemCount());
}
@Test
public void testCalculateTotalPrice_WithDiscount() {
// Arrange
ShoppingCart cart = new ShoppingCart();
cart.addItem();
DiscountService discountService = new DiscountService();
cart.setDiscountService(discountService);
// Act
double totalPrice = cart.calculateTotalPrice();
// Assert
assertEquals(9.0, totalPrice, 0.001);
}
@Test
public void testCalculateTotalPrice_WithoutDiscount() {
// Arrange
ShoppingCart cart = new ShoppingCart();
cart.addItem(); // Sepette ürün olduğunu varsayalım
// Act
double totalPrice = cart.calculateTotalPrice();
// Assert
assertEquals(10.0, totalPrice, 0.001);
}
}
- Testlerde Mantıktan Kaçının: Birim testlerinizi yazarken, el ile dize birleştirmeden,
while
,for
,switch
veif
gibi mantıksal koşullardan kaçının. Bunların testlerinizde bir hataya neden olma şansı daha düşüktür.- Aşağıdaki örnek için:
1
2
3
4
5
6
7
// Test Edilecek Sınıf
public class MathOperations {
public static int add(int a, int b) {
return a + b;
}
}
- Aşağıdaki bu test, iki sayının toplamını doğrulayan basit bir ifade içerir. Mantık içermeyen, doğrudan amacını belirten ve hata yapma olasılığı daha düşük olan bir testtir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Uygun Test
public class MathOperationsTest {
@Test
public void testAddition() {
// Arrange
int operand1 = 3;
int operand2 = 5;
// Act
int result = MathOperations.add(operand1, operand2);
// Assert
assertEquals(8, result);
}
}
- Aşağıdaki bu örnek ise, bir koşula bağlı olarak iki farklı girişi ele alır, ancak testin karmaşıklığını artırır ve buna bağlı hata yapma olasılığını artırır. Daha basit ve doğrudan bir yaklaşım her zaman tercih edilir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Uygun Olmayan Test
public class MathOperationsTest {
@Test
public void testAdditionWithConditionalLogic() {
// Arrange
int operand1 = 3;
int operand2 = 5;
boolean condition = true;
// Act
int result;
if (condition) {
result = MathOperations.add(operand1, operand2);
} else {
result = MathOperations.add(operand1, operand1);
}
// Assert
assertEquals(8, result);
}
}
- Altyapı Bağımlılıklarından Kaçınma: Birim testleri oluştururken altyapıya bağımlılıkları eklememeye özen gösterin.
Bu bağımlılıklar, testleri yavaşlatıcı ve kırılgan hale getirebilir.
- Aşağıdaki örnekte, bir dosya işleme sınıfı (
FileProcessor
) düşünelim. Bu sınıf, bir dosyadan veri okuyan ve işleyen basit bir işlevselliğe sahip olsun:
- Aşağıdaki örnekte, bir dosya işleme sınıfı (
1
2
3
4
5
6
7
8
// Test Edilecek Sınıf
public class FileProcessor {
public List<String> readLinesFromFile(String filePath) throws IOException {
Path path = Path.of(filePath);
return Files.readAllLines(path);
}
}
- Bu sınıfın testi için, altyapı bağımlılıklarından kaçınma ilkesini gözeterek, gerçek dosya sistemini kullanmaktan kaçınmak için dosya işlemlerini taklit eden bir mock obje kullanılabilir. Bu sayede, testimiz dosya işlemlerini taklit edebilir ve gerçek dosya sistemine bağımlılığı ortadan kaldırır. Bu test hızlı çalışır ve testin sonuçları bağımsızdır, çünkü gerçek dosya sistemini kullanmaktan kaçınılmıştır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Uygun Test
public class FileProcessorTest {
@Test
public void testReadLinesFromFile() throws IOException {
// Arrange
String filePath = "examplePath";
List<String> expectedLines = Arrays.asList("Line 1", "Line 2");
// Dosya işlemcisinin davranışının taklit edilmesi
FileProcessor fileProcessor = Mockito.mock(FileProcessor.class);
when(fileProcessor.readLinesFromFile(filePath)).thenReturn(expectedLines);
// Act
List<String> actualLines = fileProcessor.readLinesFromFile(filePath);
// Assert
assertEquals(expectedLines, actualLines);
}
}
- Beklenen ve Beklenmeyen Durum Testleri: Başarılı durumların yanı sıra hatalı durumları da test etmek için birim
testleri yazılmalıdır. Uygulamanın beklenmeyen durumlara karşı nasıl tepki vereceği test edilmelidir.
- Aşağıdaki örnekte, bir hesap yönetim sistemi (
AccountManager
) düşünelim. Bu sistem, hesap bakiyelerini kontrol etmeyi sağlayan bir işlevselliğe sahip olsun.
- Aşağıdaki örnekte, bir hesap yönetim sistemi (
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Test Edilecek Sınıf
public class AccountManager {
private int balance;
public AccountManager(int initialBalance) {
this.balance = initialBalance;
}
public int getBalance() {
return balance;
}
public void deposit(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("Yatırılan tutar negatif olamaz");
}
balance += amount;
}
public void withdraw(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("Para çekme tutarı negatif olamaz");
}
if (amount > balance) {
throw new IllegalStateException("Para çekme için yeterli para bulunmuyor");
}
balance -= amount;
}
}
- Bu sınıfın hem beklenen durumları (örneğin, doğru bir şekilde para yatırma ve çekme) hem de beklenmeyen durumları ( örneğin, negatif bir miktarla para yatırma veya hesaptan daha fazla para çekme) kontrol eden testleri içeren bir birim testi aşağıdaki gibi olmalıdır:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Uygun Test
public class AccountManagerTest {
@Test
public void testDeposit() {
// Arrange
AccountManager accountManager = new AccountManager(100);
// Act
accountManager.deposit(50);
// Assert
assertEquals(150, accountManager.getBalance());
}
@Test
public void testWithdraw() {
// Arrange
AccountManager accountManager = new AccountManager(100);
// Act
accountManager.withdraw(50);
// Assert
assertEquals(50, accountManager.getBalance());
}
@Test
public void testDeposit_NegativeAmount() {
// Arrange
AccountManager accountManager = new AccountManager(100);
// Act and Assert
assertThrows(IllegalArgumentException.class, () -> accountManager.deposit(-50));
}
@Test
public void testWithdraw_NegativeAmount() {
// Arrange
AccountManager accountManager = new AccountManager(100);
// Act and Assert
assertThrows(IllegalArgumentException.class, () -> accountManager.withdraw(-50));
}
@Test
public void testWithdraw_InsufficientFunds() {
// Arrange
AccountManager accountManager = new AccountManager(100);
// Act and Assert
assertThrows(IllegalStateException.class, () -> accountManager.withdraw(150));
}
}
-
Sonuç olarak, pozitif durumları test eden iki test (
testDeposit
vetestWithdraw
) ve beklenmeyen durumları kontrol eden üç test (testDeposit_NegativeAmount
,testWithdraw_NegativeAmount
,testWithdraw_InsufficientFunds
) bulunmaktadır. Beklenen durum testleri başarılı olmalıdır, ancak beklenmeyen durum testleri ile ilgili istisnaları fırlatmalıdır. Bu sayede, uygulamanın beklenmeyen durumlara nasıl tepki vereceğini test edebiliriz. -
Veri Temizliği ve Geri Dönüş: Birim testleri çalıştırdıktan sonra, kullanılan kaynakları temizlemek ve başlangıç durumuna geri dönmek önemlidir. Bu, testlerin tekrar tekrar yürütülebilirliğini artırır.
- Aşağıdaki örnekte, bir kullanıcı yönetim sistemi (
UserManager
) düşünelim ve kullanıcıları bir liste içinde tutalım.
- Aşağıdaki örnekte, bir kullanıcı yönetim sistemi (
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Test Edilecek Sınıf
public class UserManager {
private List<String> users;
public UserManager() {
this.users = new ArrayList<>();
}
public void addUser(String username) {
users.add(username);
}
public List<String> getUsers() {
return users;
}
}
- Kullanılan kaynakları temizleyerek başlangıç durumuna geri dönen ve sınıfı test eden bir birim test
sınıfı (
UserManagerTest
) aşağıdaki gibi olmalıdır:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Uygun Test
public class UserManagerTest {
private UserManager userManager;
@Before
public void setUp() {
userManager = new UserManager();
userManager.addUser("user1");
userManager.addUser("user2");
// İlk durumu hazırla
}
@After
public void tearDown() {
// Testlerden sonra kullanılan kaynakları temizle
userManager = null;
}
@Test
public void testAddUser() {
// Act
userManager.addUser("user3");
// Assert
List<String> users = userManager.getUsers();
assertEquals(3, users.size());
}
@Test
public void testGetUsers() {
// Act
List<String> users = userManager.getUsers();
// Assert
assertEquals(2, users.size());
}
}
Bu örnekte, setUp
metodu testin başlangıç durumunu hazırlar ve tearDown
metodu testlerin sonunda kullanılan
kaynakları temizler. setUp
ve tearDown
metodları JUnit
tarafından otomatik olarak çağrılır ve bu sayede her bir
testin kendi bağımsız çalışma ortamına sahip olması sağlanır.
- Test Verilerinin Yönetimi: Birim testleri için kullanılan veriler ayrı olarak yönetilmeli ve testlerin
tekrarlanabilirliği sağlanmalıdır. Değişken veri setleri kullanarak farklı senaryoları test etmek önemlidir. Farklı
senaryoları test etmek için değişken veri setleri kullanmak, uygulamanın farklı durumlara nasıl tepki vereceğini daha
iyi anlamamıza yardımcı olur.
- Aşağıdaki örnekte, bir matematik işlemleri sınıfı (
MathOperations
) olduğunu ve toplama işlemini gerçekleştiren bir metodumuz olduğunu varsayalım:
- Aşağıdaki örnekte, bir matematik işlemleri sınıfı (
1
2
3
4
5
6
7
// Test Edilecek Sınıf
public class MathOperations {
public static int add(int a, int b) {
return a + b;
}
}
- Bu
add
metodunun farklı senaryolarını test eden bir birim test sınıfı (MathOperationsTest
) aşağıdaki gibi olmalıdır:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Uygun Test
@RunWith(Parameterized.class)
public class MathOperationsTest {
private final int operand1;
private final int operand2;
private final int expectedResult;
public MathOperationsTest(int operand1, int operand2, int expectedResult) {
this.operand1 = operand1;
this.operand2 = operand2;
this.expectedResult = expectedResult;
}
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{1, 1, 2},
{2, 3, 5},
{-1, 1, 0},
{0, 0, 0},
{-5, -3, -8}
});
}
@Test
public void testAdd() {
// Arrange
MathOperations mathOperations = new MathOperations();
// Act
int result = mathOperations.add(operand1, operand2);
// Assert
assertEquals(expectedResult, result);
}
}
Bu örnekte, MathOperationsTest
sınıfı farklı senaryoları test etmek için JUnit’un Parameterized
özelliğini
kullanmaktadır. data
metodu, farklı test senaryolarını içeren bir veri seti döndürür. Her bir test senaryosu için
bir test çalıştırılır ve beklenen sonuçlarla gerçek sonuçlar karşılaştırılır.
- Kod Tekrarından Kaçının: Birim testlerinizde, aynı kod bloklarını veya test senaryolarını tekrarlamaktan kaçının.
Kod tekrarı, bakımı zorlaştırabilir ve güncelleştirmelerde sorunlara neden olabilir. Ortak kod parçalarını ayırarak ve
yeniden kullanarak, hem kodunuzun daha okunabilir ve yönetilebilir olmasını sağlayabilirsiniz hem de
güncelleştirmelerde tek bir yerde değişiklik yaparak tüm test senaryolarını etkileyebilirsiniz. Böylece, kod
tekrarından kaçınarak testlerinizin bakımını kolaylaştırabilir ve geliştirme sürecinde verimliliği artırabilirsiniz.
- Aşağıdaki örnekte,
UserManagerTest
sınıfında her bir test metodu için aynıUserManager
örneğini oluşturmak için@BeforeEach
işlevini kullanıyoruz. Böylece her test başlamadan öncesetUp()
metodunu kullanarak test için hazırlıkları gerçekleştiriyoruz. Bu yaklaşım, kod tekrarını önlemeye ve her bir test metodu için aynı hazırlık adımlarını tekrarlamaktan kaçınmamıza yardımcı olur. Bu da kodun bakımını kolaylaştırır ve okunabilirliğini artırır.
- Aşağıdaki örnekte,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Uygun Test
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class UserManagerTest {
private UserManager userManager;
private User testUser1;
private User testUser2;
@BeforeEach
public void setUp() {
// Arrange
userManager = new UserManager();
testUser1 = new User("name1");
testUser2 = new User("name2");
}
@Test
public void testAddUser() {
// Act
int result = userManager.addUser(testUser1);
// Assert
assertEquals(expectedResult, result);
}
@Test
public void testRemoveUser() {
// Arrange
userManager.addUser(testUser1);
// Act
int result = userManager.removeUser(testUser1);
// Assert
assertEquals(expectedResult, result);
}
@Test
public void testUpdateUser() {
// Arrange
userManager.addUser(testUser2);
testUser2.setName("name3");
// Act
int result = userManager.updateUser(testUser2);
// Assert
assertEquals(expectedResult, result);
}
}
5. Birim Testi Yazımında Kötü Pratikler (Yapılmaması Gerekenler)
Birim testlerinde yaygın olarak yapılan bazı hatalar, testlerin etkinliğini ve bakımını olumsuz etkileyebilir. Bu hatalar, testlerin güvenilirliğini azaltmanın yanı sıra, yazılım testlerinin bakım maliyetlerini artırabilir ve yazılıma olan güveni azaltabilir. Sıklıkla yapılan kaçınılması gereken hatalar aşağıda verilmiştir.
Test Performansını İhmal Etmek
- Yavaş çalışan testler, hızlı geri bildirim almayı zorlaştırır ve geliştirme sürecini engelleyebilir.
- Test süitinin yavaş çalışması, geliştiricilerin sık sık ve hızlı bir şekilde testleri çalıştırmasını engelleyebilir.
- Yazılım projesinin sonuna doğru birim test sayımız oldukça fazla olacağı için projenin başından itibaren testlerin performansına dikkat edilmelidir.
Örneğin, entegrasyon testleri gibi uzun süren testleri birim test sürecine dâhil etmek, bu da geliştiricilerin hızlı bir şekilde geri bildirim almasını engelleyebilir.
Sık Testten Kaçınma
- Yavaş çalışan testler, geliştiricilerin testleri sıkça çalıştırmaktan kaçınmasına neden olur. Bu, hataların daha geç fark edilmesine ve düzeltilmesinin daha zor ve maliyetli hale gelmesine yol açar.
Çalıştırılmasının Zorluğu
- Testlerin, dış bağımlılıklara fazla bağlı olması, bu bağımlılıkların yönetilmesini ve testlerin çalıştırılmasını zorlaştırır.
- Test süreci, dış bağımlılıklar nedeniyle kararsız veya ağır olabilir.
Örneğin, testler için veri tabanı sunucularının, ağ bağlantılarının vb. başlatıldıktan sonra çalıştırılması gereken durumlar test etmekten kaçınmaya sebep olabilir.
Sorunların Geç Tespiti
- Hızlı ve doğru geri bildirim sağlamayan testler, hataların erken evrelerde tespit edilmesini engeller. Testler, geliştirme süresince geliştiriciye yardımcı olması beklenirken etkisiz kod parçalarına dönmüşlerdir.
- Hataların daha derinlemesine yerleşmesine ve daha karmaşık hale gelmesine yol açar.
Örneğin, yeni eklenen özellik mevcut sistemle uyumsuz olabilir veya beklenmedik yan etkilere neden olabilir. Normalde, bu tür sorunlar, iyi tasarlanmış ve hızlı çalışan test süreçleriyle kolayca ve erken bir aşamada tespit edilebilir. Ancak, yetersiz test süreçleri nedeniyle bu hatalar gözden kaçabilir ve kod ana sisteme entegre edilir.
Geliştirme Sürecinin Verimsizliği
- Yavaş çalışan ve etkili geri bildirim sağlamayan testler, geliştirme sürecinin verimliliğini düşürür. Geliştiriciler, kodlarını hızlı ve güvenilir bir şekilde doğrulayamazlar.
Hata Düzeltme Maliyetinin Artması
- Yanlış pozitif testler, aslında hatalı olan kodun testlerden başarıyla geçmesini ifade eder. Yanlış pozitif sonuçlar üreten testler, hataların erken aşamada tespit edilip düzeltilmesini engeller, bu da hata düzeltme maliyetlerinin zamanla artmasına neden olur.
Örneğin, bir web uygulaması geliştirme projesinde, ödeme işlemini test etmek için yazılan otomatik testler yanlış pozitif sonuçlar üretiyor; yani, aslında hatalı olan kodlar testten başarıyla geçiyor. Testler ödeme işleminin başarılı olduğunu gösteriyor, fakat gerçekte bazı ödemeler teknik sorunlar nedeniyle başarısız oluyor.
Çok Fazla Kod Satırı İçermesi
- Testlerde gereğinden fazla kod satırı bulunması, testin okunabilirliğini ve anlaşılabilirliğini azaltır. Test ne kadar kısa ve odaklıysa, o kadar okunaklı ve anlaşılır olur.
Değiştirmesi Zor
- Testin yapısı karmaşık veya çok büyük olduğunda, gerektiğinde testi değiştirmek zorlaşır. Testlerin basit ve modüler olması, bakım ve güncelleme işlemlerini kolaylaştırır.
- Aynı kod gibi “refaktör” ederek testlerinizi de güncel tutun.
Dış Bağımlılıklara Aşırı Bağlılık
- Test, gereksiz dış bağımlılıklara sahipse, bu bağımlılıkları yönetmek için ekstra çaba ve zaman gerektirir. Bu, testlerin güvenilirliğini ve tutarlılığını etkiler.
Örneğin, bir mobil uygulama geliştiriyorsunuz ve bu uygulama, hava durumu verilerini göstermek için bir dış API’ye bağlanıyor. Test süreciniz, gerçek hava durumu verilerini almak için bu dış API’ye doğrudan bağımlı hale getirilmiş. Ancak, bu API zaman zaman bakım nedeniyle kullanılamaz olabiliyor veya yanıt süreleri değişkenlik gösterebiliyor. Doğrudan bir bağımlılık yerine mock işlemleri ile test iyileştirilmelidir.
Kodun Gerçek Dünya Senaryolarını Yansıtmama
- Test, kodun gerçek dünya senaryolarını ve kullanıcıların gerçek ihtiyaçlarını yansıtmıyorsa, bu testler sistemin gerçek performansını ve davranışını doğru bir şekilde değerlendiremez.
Örneğin, gerçekte kullanıcılar çeşitli ağ sorunlarıyla karşılaşabilir, hatalı bilgi girebilir. Testler bu tür senaryoları dikkate almadığında, uygulamanın gerçek dünyadaki performansı ve davranışı doğru bir şekilde değerlendirilemez.
Özel Metotlara Odaklanma
- Özel metotları (
private methods
) doğrudan test etmek, birim testlerinin genel ilkesine aykırıdır. - Testler, genellikle sistemin dıştan görünen (
public
) gözlemlenebilir davranışlarına odaklanmalıdır. - Özel metotları doğrudan test etmek, testleri uygulamanın iç yapısına fazla bağlar ve yeniden düzenlemeye karşı direnci zayıflatır.
- Sınıflarınızın
public
API’lerini ve açık olan (ya da öyle görünen) davranışlarını test edin.
Yetersiz Kapsam ve Ölü Kod
- Bir özel metot çok karmaşık olduğunda ve bu metot, genel API üzerinden yeterli bir şekilde test edilemiyorsa, bu iki soruna işaret edebilir: ölü kod veya eksik soyutlama.
- Ölü kod, gereksiz ve kullanılmayan koddur ve silinmelidir.
- Eksik soyutlama, karmaşık bir özel metodu daha iyi yönetmek için ayrı bir sınıfa çıkarılmalıdır.
Alan Bilgisinin Testlere Sızdırılması
- Testlerde, test edilen algoritmanın detaylarının tekrar edilmesi, testin yeniden düzenlemeye karşı dirençli olmadığını gösterir.
- Testler, algoritmanın iç yapısını taklit etmek yerine, beklenen sonuçları doğrulamalıdır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CalculatorTests {
@Test
public void addingTwoNumbers() {
int value1 = 1;
int value2 = 3;
int expected;
Calculator calculator = new Calculator();
int actual = calculator.add(value1, value2);
expected = value1 + value2;
assertEquals(expected, actual);
}
}
Alan Bilgisinin Teste Sızdırılması
Tek Bir Birim Testinde Birden Çok Koşulu Test Etmek
- Tek bir birim testinde sadece bir metodun davranışı test edilmeli. Test metodu içinde birden çok koşul ve davranış test edilmemelidir.
- Birden çok koşulun aynı metot içerisinde test edilmesi, testlerin anlaşılmasını ve bakımını zorlaştırır.
- Birden çok davranışın test edilmesi
SOLID
prensiplerinden Single Responsibility prensibine aykırıdır.
Örneğin, bir Kullanıcı sınıfı için kullanıcının e-posta ve parola formatlarını aynı anda doğrulayan bir test düşünün.
Eğer bu test başarısız olursa, e-posta formatının mı, parola formatının mı, yoksa her ikisinin mi yanlış olduğunu
bilemezsiniz. Daha iyi bir yaklaşım, bunu iki ayrı teste bölmek olurdu: biri e-posta, diğeri parola için. Bu şekilde,
her test Kullanıcı sınıfının belirli bir yönünü doğrulamaktan sorumlu olur ve Single Responsibility
prensibine uygun
hareket edilmiş olur.
Başka bir örnek olarak, bir API gönderi gövdesindeki bazı parametreleri alır ve bir Çalışan kaydı oluşturur ve yanıt olarak onu döndürür. Bu Çalışan nesnesi, ad, soyadı, adres vb. gibi birden fazla alana sahip olabilir. Yalnızca adı doğrulamak için bir test senaryosu yazmak, ardından soyadı için başka bir tane ve adres için başka bir tane yazmak, herhangi bir değer olmadan kod çoğaltmaktır ve yapılmamalıdır. Bunun yerine, çalışan oluşturmak ve bu birim testindeki tüm alanları doğrulamak için tek bir pozitif test senaryosu yazmak gerekir.
Birim Testlerini, Birim Seviyesinde Tutun
- Birim testlerinin, entegrasyon testlerine dönüşmesine izin vermemelisiniz.
- Birim testlerinde bir metodun bir davranışını test etmelisiniz.
İlgisiz Detayları Test Etmek
- Fonksiyonun davranışına veya çıktısına katkıda bulunmayan yönleri test etmeyin.
Örnek: Davranıştan ziyade uygulamanın detayları olan özel metodları aşırı test etmek.
Yetersiz İstisna Testi
- Kodunuzun istisnaları nasıl ele aldığını test etmelisiniz.
Örnek: Bir dosya okuyan ancak dosya bulunamadı veya erişim reddedildi senaryolarını içeren testler yazmalısınız.
Sınır Değerleri Test Etmemek
- Sadece genel duruma odaklanmak ve uç durumları veya hata koşullarını ihmal etmek, kod tabanınızın önemli bölümlerinin
test edilmemiş olmasına neden olabilir.
- Sınır değerleri test etmemek, yaygın olmayan ama potansiyel olarak kritik olan senaryolardaki hataların tespit edilememesine yol açabilir.
Örnek: Yalnızca pozitif sayılarla bir metodu test etmek, negatif veya sıfır girişlerini göz ardı etmek.
Birim Testlerini İzole Etmemek
- Birim testleri, tek bir iş birimini izole şekilde test etmelidir.
- Birim testlerini izole etmemek, dış faktörlerin etkisinde kalan ve güvenilmez ve yavaş olan testlere yol açabilir.
Örnek: Bir testin başarısının önceki bir testin bir veri tabanını değiştirmesine bağlı olması. Bu duruma @AfterEach
metodu ile her test sonrası eski haline döndürülmeyen veriler örnek verilebilir.
Kapsam Oranı Yanıltıcı Olabilir
%100 kapsam
, kodunuzun hatasız olduğu anlamına gelmez. Kapsam yalnızca kodun hangi bölümünün yürütüldüğünü gösterir. Her koşulda çalışacağını garanti etmez ve belirli parametrelerin değerleri, bazı uygulama durumları veya eşzamanlılık sorunları nedeniyle yine de başarısız olabilir.%100 test kapsamı
aşırı mühendislik olarak düşünülebilir ve gereksizdir. Çünkü proje büyüdükçe test bakım maliyeti artacak, testlerin her yeni geliştirme ile yönetilmesi geliştirici için zorlaşacaktır.
Çoklu Arrange, Act ve Assert Uygulaması
- Bir test içerisinde sırasıyla arrange-act-assert adımları uygulandıktan sonra tekrardan act ve arrange aşamasına
dönülmemelidir.
- Bu durum, testin birden fazla şeyi aynı anda doğruladığını gösterir ve bu tür testler ayrılmalıdır.
- Testlerin arrange aşamasındaki oluşturulan verilerin ve izlenilen adımların azaltılması için Text Fixture terimi kullanılır. Arrange kısmının ayrı bir yere çıkarılması testlerin daha kısa ve sade olmasını sağlar. Ancak dikkat edilmesi gereken durumlar vardır.
Örnek: Constructor ile istenilen verileri oluşturabilir. Bu yaklaşım test kodunun miktarını azaltabilir, ancak testler arasında yüksek bağlantı oluşturur ve testin okunabilirliğini azaltır. Özellikle testler arasında yüksek bağlantının bir anti-pattern olduğunu belirtir.
- Başka bir örnek,
@Before
ve@BeforeEach
gibi anotasyonlar ile test paketinizdeki her birim testinden önce Setup çağrılır. Bazıları bunu yararlı bir araç olarak görse de genellikle şişirilmiş ve okunması zor testlere yol açmaktadır. Testin çalışır duruma getirilmesi için her testin genellikle farklı gereksinimleri olacaktır. Ne yazık ki, Setup sizi her test için tam olarak aynı gereksinimleri kullanmaya zorlamaktadır.
Test Bakımını İhmal Etmek
- Kod tabanı geliştikçe testleri güncellememek veya yeniden yapılandırmamak, kırılgan ve güvenilmez testlere yol açabilir.
Örnek: Bir metodun mantığını değiştirdikten sonra testleri güncellememek, artık metodu doğru bir şekilde test etmeyen eski testlere yol açar.
Regresyona Karşı Dirençsizlik
- Regresyon, yazılımda yeni bir işlevsellik ekledikten sonra bir özelliğin amaçlandığı gibi çalışmamasına neden olan bir yazılım hatasıdır.
- İyi bir regresyon koruması olmadan, projenin uzun vadede sürdürülebilirliği tehlikeye girebilir.
- Test, kod tabanının önemli kısımlarını kapsamazsa, regresyonları tespit etme yeteneği sınırlı demektir. Önemli işlevler, sınıflar veya modüller test dışı bırakılırsa, bu alanlarda meydana gelebilecek hatalar gözden kaçabilir.
Yetersiz Test Kapsamı
- Yetersiz test kapsamı, uygulamanın önemli bölümlerinin test edilmemiş kalmasına neden olabilir. Kritik yollar ve karmaşık mantık kapsamlı bir şekilde test edilmelidir.
- Regresyonlardan doğacak hatalar yetersiz test kapsamı nedeniyle geliştirme aşamasında saptanamaz.
Koşul İfadelerinden Kaçının
- İster birim testi ister entegrasyon testi olsun, bir test dallara ayrılmayan basit bir adım dizisinden oluşmalıdır.
- Koşul ifadesi, testin aynı anda çok fazla şeyi doğruladığını gösterir. Bu nedenle böyle bir testin birkaç teste bölünmesi gerekir.
- Bir test içinde dallanmanın hiçbir faydası yoktur. Ek bakım maliyetine sebep olur, testlerin okunmasını ve anlaşılmasını zorlaştırır.
- Bir test içerisinde bir defa arrange-act-assert aşamalarının kullanılmasına gerekliliğine aykırı bir davranış sergiler.
Müşterinin adını, soyadını ve üyelik bilgilerini alıp buna göre indirim oranı sağlayan bir metodun kötü ve iyi yaklaşımla testleri verilmiştir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestClass {
@Test
public void testDiscount() {
Customer customer = new Customer("John Doe", "GOLD");
if (customer.getStatus().equals("GOLD")) {
assertEquals(0.2, customer.getDiscount(), 0.01);
} else if (customer.getStatus().equals("SILVER")) {
assertEquals(0.1, customer.getDiscount(), 0.01);
} else {
assertEquals(0.0, customer.getDiscount(), 0.01);
}
}
}
Koşul İfadelerine Bağlı Kötü Yaklaşım
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestClass {
@Test
public void testGoldCustomerDiscount() {
Customer customer = new Customer("John Doe", "GOLD");
assertEquals(0.2, customer.getDiscount(), 0.01);
}
@Test
public void testSilverCustomerDiscount() {
Customer customer = new Customer("Jane Doe", "SILVER");
assertEquals(0.1, customer.getDiscount(), 0.01);
}
@Test
public void testNormalCustomerDiscount() {
Customer customer = new Customer("Joe Bloggs");
assertEquals(0.0, customer.getDiscount(), 0.01);
}
}
Koşul İfadelerine Bağlı İyi Yaklaşım
Kodun Refaktörlenmesine Karşı Direnç
- Bir önceki maddelerde bahsettiğimiz gibi, yanlış pozitifler bir testin başarısız olduğunu gösterirken aslında test edilen kodun doğru çalıştığını ifade eden bir durumdur. Bu, testin hatalı olduğunu gösterir.
- Kötü yapılandırılmış testler, refaktörleme (yeniden düzenleme) sırasında yanlış pozitif sonuçlar üreterek kodun refaktörleme sürecini zorlaştırabilir.
- Geliştiriciler, testlerin doğruluğuna güvenmek ister. Yanlış pozitifler, testlerin güvenilirliği konusunda şüphe uyandırabilir.
- Yanlış pozitifler, gerçek bir problem olmadan hata düzeltme çabalarına neden olabilir, bu da zaman ve kaynak israfına yol açar.
- Yanlış pozitiflerin çokluğu, gerçek hataların göz ardı edilmesine veya önemsiz görülmesine neden olabilir.
- Kodda yapılacak değişiklikler ve iyileştirmeler, yanlış pozitifler nedeniyle gereksiz yere zorlaşabilir.
Örneğin, bir finansal uygulamada belirli bir tarihte gerçekleşen işlemlerin toplamını hesaplayan bir fonksiyon düşünün. İlk test durumunda, bu fonksiyon belirli bir tarih için test edilir ve test, beklenen sonucu sert kodlanmış bir değerle karşılaştırır. Örneğin, test 1 Ocak 2023 tarihindeki işlemlerin toplamının 1000 birim olduğunu varsayar ve fonksiyon bu değeri döndürdüğünde test başarılı olur. Ancak, işlemler zamanla güncellendiğinde ve bu tarihteki toplam işlem tutarı değiştiğinde, test hala eski değeri bekler ve başarısız olur. Örneğin, 1 Ocak 2023 tarihindeki toplam işlem tutarı artık 1000 birim değil, 1200 birimdir fakat test hala 1000 birim olmasını beklemektedir. Bu, yanlış bir pozitiftir ve testin gerçekte hatalı olduğunu gösterir.
Testleri Uygulama Detaylarına Sıkı Sıkıya Bağlamak
- Testler, kodun davranışını ele alarak yazılmalıdır, uygulama detaylarına göre yazılmamalıdır.
- Uygulamanın davranışı aynı kaldığı sürece, uygulamadaki değişiklikler başarısız bir teste yol açmamalıdır. Test sonuçları her zaman aynı çıkmalıdır.
- Testler, kodun iç yapısına fazla bağımlıysa, her kod değişikliğinde testlerin de güncellenmesi gerekebilir. Bu durum, testlerin bakımını zorlaştırır ve esneklikten yoksun testler yaratır.
Örneğin, bir sınıfınız var ve bu sınıf, birkaç metot çağrısını içeren bir işlevi gerçekleştiriyor. Eğer testiniz, bu metodların tam olarak hangi sırada ve kaç kere çağrıldığını doğruluyorsa, bu test uygulamanın iç yapısına aşırı bağlı hale gelmiş demektir. Davranışsal testler yazarak ve mock nesneleri kullanırken metot çağrılarının sırası ve sayısı yerine metotların beklenen sonuçları üzerine odaklanarak bu sorunu azaltabilirsiniz.
Açık Bir Amaç Olmadan Test Yazmak
- Her testin var olma nedeni açık olmalıdır. Belirli bir amaç olmadan yazılan testler, kafa karışıklığına veya gereksiz yere çok sayıda testin bulunmasına neden olabilir, bu da bakımı zorlaştırır. Bu durumda test adlandırmalarının da önemi vardır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestClass {
@Test
public void addingTwoNumbers() {
int value1 = 1;
int value2 = 3;
int expected;
Calculator calculator = new Calculator();
int actual = calculator.add(value1, value2);
expected = value1 + value2;
assertEquals(expected, actual);
}
}
Amaçsız Test Yazımı Kötü Örnek
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestClass {
@Test
public void testToplaPozitifSayilar() {
Calculator calculator = new Calculator();
assertEquals("Pozitif sayılarla toplama hatası", 8, calculator.add(3, 5));
}
@Test
public void testCarpNegatifSayilar() {
Calculator calculator = new Calculator();
assertEquals("Negatif sayılarla çarpma hatası", 6, calculator.topla(-2, -3));
}
}
Amaçsız Test Yazımı İyi Örnek
Benzer Test Durumları İçin Parametreli Testler Kullanmamak
- Farklı girdilerle benzer senaryoları test ederken, parametreli testler kullanmak verimlidir. Bunu yapmamak, tekrarlayan kod ve daha zor bakıma yol açabilir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CalculatorTests {
@Test
public void addingTwoNumbersWithFirstDataSet() {
int value1 = 1;
int value2 = 3;
int actual = Calculator.add(value1, value2);
assertEquals(4, actual);
}
@Test
public void addingTwoNumbersWithSecondDataSet() {
int value1 = 11;
int value2 = 33;
int actual = Calculator.add(value1, value2);
assertEquals(44, actual);
}
}
Parameterized İle Yapılmamış Test
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CalculatorTests {
@ParameterizedTest
@CsvSource({
"1, 3, 4",
"11, 33, 44",
"100, 500, 600"
})
public void addingTwoNumbers(int value1, int value2, int expected) {
int actual = Calculator.add(value1, value2);
assertEquals(expected, actual);
}
}
Paramaterized Test
Testler Arasındaki Karşılıklı Bağımlılıktan Kaçının, Aşırı Mocklama (Sahte Nesne Kullanımı)
- Mocklama çerçevelerinin aşırı kullanımı, anlaşılması ve bakımı zor testlere yol açabilir.
- Aşırı mocklama ayrıca, nesneler arası gerçek etkileşimlerin etkin bir şekilde test edilmediği anlamına da gelebilir.
- Testler, mock ve stub kullanımında aşırıya kaçarsa, gerçek sistem davranışını doğru bir şekilde yansıtmayabilir. Bu, testlerin gerçek senaryolara karşı dayanıklılığını azaltır.
- Mock’ların kullanıldığı bir testi okumaya çalışıyorsanız ve testi anlamak için kendinizi zihinsel olarak test edilen kodun üzerinden geçerken buluyorsanız, o zaman muhtemelen mock’ları aşırı kullanıyorsunuz demektir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class testClass {
@Test
public void testOrderPaymentIsProcessed() {
paymentProcessor = new PaymentProcessor(mockPaymentServer);
when(mockPaymentServer.isServerAvailable()).thenReturn(true);
when(mockPaymentServer.startPaymentSession()).thenReturn(mockSessionManager);
when(mockSessionManager.getSession()).thenReturn(session);
when(mockPaymentServer.processPayment(session, orderDetails, amount)).thenReturn(mockPaymentResponse);
when(mockPaymentResponse.isSuccessful()).thenReturn(true);
paymentProcessor.processOrderPayment(orderDetails, Money.dollars(amount));
verify(mockPaymentServer).processPayment(session, orderDetails, amount);
}
}
Aşırı Mock Kullanımı Olan Test
1
2
3
4
5
6
7
8
9
10
11
public class testClass {
@Test
public void testOrderPaymentIsProcessed() {
paymentProcessor = new PaymentProcessor(paymentServer);
paymentProcessor.processOrderPayment(orderDetails, Money.dollars(500));
assertEquals(500, paymentServer.getMostRecentCharge(orderDetails));
}
}
Aşırı Mock Kullanımını Önlemek İçin Daha Efektif Yazılmış Bir Test
Test Okunabilirliğini İhmal Etmek
- Testler, kodunuzun anlaşılır bir belgesi olmalıdır. Eğer testler kolay okunup anlaşılmıyorsa, bu genellikle projenin kodlarında da bir karmaşıklığın olduğunun göstergesidir.
- Testlerin, ilk bakışta neyi test ettiği açıkça anlaşılabilir olmalıdır. İsimlendirme hataları, testlerin okunabilirliğini önemli ölçüde azaltır. Örneğin, test metotlarında karmaşık mantık kullanmak veya açık olmayan isimlendirmeler yapmak, diğer geliştiricilerin neyin test edildiğini anlamasını zorlaştırır.
İsimlendirme Hataları
- Test ismi, testin neyi test ettiğini net bir şekilde ifade etmeli. Hangi metodun test edildiğini, hangi durumun kontrol edildiğini ve beklenen sonucun ne olduğunu belirtmelidir.
- İsim, yeterince açıklayıcı olmalı ama aynı zamanda fazla uzun olmamalı. Gereksiz ayrıntılardan kaçının.
- Tüm testleriniz benzer bir isimlendirme yapısına sahip olmalı. Bu, testler arasında gezinmeyi ve anlamayı kolaylaştırır.
- Test isimleri, belirli bir senaryoyu veya kullanım durumunu yansıtmalıdır.
- Hem olumlu hem de olumsuz senaryoları test etmek önemlidir. İsimlerde bu ayrımın yapılması faydalıdır.
Örneğin, test1()
, testEmail()
gibi test edilen sistemi, test edilen senaryo durumunu ve nasıl bir sonucu olacağını
(başarılı, başarısız veya hata fırlatma gibi) içinde barındırmayan isimlendirmeler iyi pratikler değildir.
6. SONUÇ
Birim testleri, yazılım geliştirme sürecinin kritik bir bileşenidir ve yüksek kaliteli yazılım ürünlerinin geliştirilmesinde önemli bir role sahiptir. İyi tasarlanmış birim testleri, sistemlerin daha sağlam ve sürdürülebilir olmasını sağlar. Bu testler, fonksiyonel hataların erken evrelerde tespit edilmesini, regresyon hatalarının önlenmesini ve kod refaktörünün güvenli bir şekilde yapılmasını mümkün kılar.
Birim testlerinin etkili şekilde kullanılması, yazılımın modülerliğini ve tekrar kullanılabilirliğini artırır, geliştirme sürecindeki hataların düzeltilme maliyetini azaltır. Test Odaklı Geliştirme (TDD) gibi metodolojiler, yazılımın tasarımını ve yapısını optimize ederek geliştiricilere sürekli bir geri bildirim döngüsü sağlar. Ayrıca, birim test çerçevelerinin ve araçlarının stratejik seçimi, test süreçlerinin etkinliğini önemli ölçüde artırabilir. Bu araçlar, geliştirme sürecinin çeşitli aşamalarında kodun bütünlüğünü korumaya yardımcı olurken, Sürekli Entegrasyon (CI) ortamlarında otomatik testlerin rahatlıkla uygulanabilmesini sağlar.
Birim test yazımında karşılaşılan sık hatalardan kaçınmak ve en iyi pratikleri uygulamak, kodun sürekli olarak gözden geçirilmesini ve iyileştirilmesini gerektirir. Geliştiriciler, kodunun yeniden kullanılabilirliğini ve okunabilirliğini artıracak şekilde testleri stratejik olarak planlamalı ve uygulamalıdır. Bu yaklaşım, yazılımın yaşam döngüsü boyunca performansını ve güvenilirliğini sürdürmek için esastır.
Yazımızın teknik gözden geçirmesi için Anıl Özberk ve İbrahim Özcan’a, editör desteği için ise Kübra Ertürk’e teşekkür ederiz.
7. KAYNAKÇA