Birim Testleri Kılavuzu

İçindekiler

  1. Giriş
  2. Birim Testi Temelleri
  3. Birim Testi Çerçeveleri ve Araçları
  4. Birim Testinde Uygulanan İyi Pratikler
  5. 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.

Testlerin yazılım geliştirme sürecine etkisini gösteren grafikler

Ş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.

Kod Kapsamı
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.

Dal Kapsamı
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.

Klasik ve Londra Yaklaşımı

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:

  1. Test paketine bir test ekleyin.
  2. (Kırmızı) Yeni testin başarısız olduğundan emin olmak için tüm testleri çalıştırın.
  3. (Yeşil) Tek testi geçmek için yeterli kod yazın.
  4. Tüm testleri çalıştırın.
  5. (Refactor) Testleri yeşil tutarken ilk kodu okunabilir ve bakımı kolay hale getirmek için güvenli bir şekilde iyileştirin.
  6. Tekrarla.
Kırmızı-Yeşil-Refactor Şeması

Ş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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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>
  1. 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.

  2. 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.

  3. 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.

  4. 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.
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çin addUser metodunu içerir. Test sınıfındaki testAddUser 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.
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ın calculateTotalPrice 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 ve testSubtraction. 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.
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çin DatabaseConnection 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ı yapmadan UserService‘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.
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:

      1. 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.

      2. 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.

      3. 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.

      4. Paralel Test Yürütme: Eğer mümkünse, testleri paralel olarak yürüterek süreyi kısaltabilirsiniz.

      5. 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.
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 ve if 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:
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.
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 ve testWithdraw) 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.
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:
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 önce setUp() 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.
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

  1. Unit Testing Best Practices - Brightsec
  2. Unit Testing Best Practices - Microsoft
  3. What is Unit Testing? - Spiceworks
  4. JUnit 5 - JUnit
  5. pytest Documentation
  6. RSpec Documentation
  7. NUnit - NUnit
  8. Mocha - Mocha
  9. Test-Driven Development - Wikipedia
  10. Test-Driven Development - Test Driven