← Kursa Dön
📄 Text · 18 min

Unit Testing: Google Test (gtest)

Bir köprü inşa eden mühendis, köprüyü trafiğe açmadan önce her çelik kirişi, her cıvatayı, her kaynak noktasını test eder. Yük testleri yapar, rüzgar simülasyonları çalıştırır, deprem senaryolarını dener. Hiçbir mühendis "bence sağlam" deyip köprüyü açmaz. Ama yazılımda öyle yapıyoruz — kodu yazıyoruz, bir iki kez elle deniyoruz, "çalışıyor gibi" deyip production'a atıyoruz. Sonra gece 3'te telefon çalıyor.

Unit testing, kodunun en küçük parçalarını (fonksiyonlar, sınıflar) otomatik olarak doğrulama işlemidir. Elle test etmek yerine, test kodun senin için kontrol eder — her build'de, her commit'te, her gece. Bu derste C++ dünyasının en yaygın test framework'ü olan Google Test (gtest) ile tanışacağız, test nasıl yazılır öğreneceğiz ve Google Mock ile bağımlılıkları nasıl taklit edeceğimizi göreceğiz.


Neden Test Yazarız?

Elle Test Etmenin Sınırları

Diyelim ki bir hesapla() fonksiyonu yazdın. Birkaç değerle denedin, doğru sonuç verdi. Ama:

  • Negatif sayılarla denedin mi?

  • Sıfıra bölme durumunu test ettin mi?

  • Çok büyük sayılarla overflow olur mu?

  • Birisi 3 ay sonra fonksiyonu değiştirdiğinde, eski davranışın bozulmadığını kim kontrol edecek?

Elle test etmek tekrarlanamaz, eksik ve zaman kaybıdır. Otomatik testler ise bir kez yazılır, binlerce kez çalıştırılır.

Test Yazmanın Faydaları

  1. Regresyon koruması: Yeni özellik eklerken eski şeylerin bozulmadığını garanti eder

  2. Canlı dokümantasyon: Testler, kodun nasıl kullanılması gerektiğini gösterir

  3. Cesaretle refactor: Testlerin varsa, kodu güvenle yeniden yapılandırabilirsin

  4. Daha iyi tasarım: Test yazılabilir kod, doğal olarak daha modüler olur

TDD Kısaca

Test-Driven Development (TDD), önce testi yazıp sonra kodu yazma disiplinidir:

  1. Red: Başarısız bir test yaz (henüz kod yok)

  2. Green: Testi geçirecek minimum kodu yaz

  3. Refactor: Kodu temizle, testlerin hâlâ geçtiğinden emin ol

TDD'yi her zaman uygulaman gerekmez ama disiplini öğrenmek tasarım anlayışını geliştirir.


Google Test Kurulum

CMake ile Entegrasyon

Google Test'i projeye eklemenin en temiz yolu FetchContent:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)

# Google Test'i indir ve ekle
include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        v1.14.0
)
FetchContent_MakeAvailable(googletest)

# Ana kütüphane (test edilecek kod)
add_library(mylib src/hesap_makinesi.cpp)
target_include_directories(mylib PUBLIC include/)

# Test çalıştırılabilir dosyası
enable_testing()

add_executable(testler
    tests/hesap_makinesi_test.cpp
    tests/string_utils_test.cpp
)
target_link_libraries(testler
    mylib
    GTest::gtest_main    # main() fonksiyonunu gtest sağlar
    GTest::gmock         # Google Mock
)

include(GoogleTest)
gtest_discover_tests(testler)

Proje yapısı:

projem/
├── CMakeLists.txt
├── include/
│   └── hesap_makinesi.h
├── src/
│   └── hesap_makinesi.cpp
└── tests/
    └── hesap_makinesi_test.cpp

Build ve test çalıştırma:

mkdir build && cd build
cmake ..
cmake --build .
ctest --output-on-failure

İlk Testleri Yazmak: TEST Makrosu

Test Edilecek Kod

// include/hesap_makinesi.h
#pragma once
#include <stdexcept>
#include <cmath>

class HesapMakinesi {
public:
    double topla(double a, double b) const { return a + b; }
    double cikar(double a, double b) const { return a - b; }
    double carp(double a, double b) const { return a * b; }
    
    double bol(double a, double b) const {
        if (b == 0.0) {
            throw std::invalid_argument("Sifira bolme hatasi");
        }
        return a / b;
    }
    
    double usAl(double taban, int us) const {
        return std::pow(taban, us);
    }
    
    bool asal(int n) const {
        if (n < 2) return false;
        for (int i = 2; i * i <= n; ++i) {
            if (n % i == 0) return false;
        }
        return true;
    }
};

TEST Makrosu

TEST(TestSuiteName, TestName) en temel test makrosudur. İlk parametre test grubunu (suite), ikincisi spesifik testi adlandırır:

// tests/hesap_makinesi_test.cpp
#include <gtest/gtest.h>
#include "hesap_makinesi.h"

// === Test Suite: Toplama ===
TEST(ToplamaTest, IkiPozitifSayi) {
    HesapMakinesi hesap;
    EXPECT_DOUBLE_EQ(hesap.topla(2.0, 3.0), 5.0);
}

TEST(ToplamaTest, NegatifSayilar) {
    HesapMakinesi hesap;
    EXPECT_DOUBLE_EQ(hesap.topla(-2.0, -3.0), -5.0);
}

TEST(ToplamaTest, SifirIle) {
    HesapMakinesi hesap;
    EXPECT_DOUBLE_EQ(hesap.topla(42.0, 0.0), 42.0);
}

// === Test Suite: Bolme ===
TEST(BolmeTest, NormalBolme) {
    HesapMakinesi hesap;
    EXPECT_DOUBLE_EQ(hesap.bol(10.0, 2.0), 5.0);
}

TEST(BolmeTest, SifiraBolmeException) {
    HesapMakinesi hesap;
    EXPECT_THROW(hesap.bol(10.0, 0.0), std::invalid_argument);
}

TEST(BolmeTest, OndalikSonuc) {
    HesapMakinesi hesap;
    EXPECT_NEAR(hesap.bol(10.0, 3.0), 3.3333, 0.001);
}

// === Test Suite: Asal ===
TEST(AsalTest, KucukSayilar) {
    HesapMakinesi hesap;
    EXPECT_FALSE(hesap.asal(0));
    EXPECT_FALSE(hesap.asal(1));
    EXPECT_TRUE(hesap.asal(2));
    EXPECT_TRUE(hesap.asal(3));
}

TEST(AsalTest, BuyukAsallar) {
    HesapMakinesi hesap;
    EXPECT_TRUE(hesap.asal(97));
    EXPECT_TRUE(hesap.asal(7919));
    EXPECT_FALSE(hesap.asal(100));
}

TEST(AsalTest, NegatifSayilar) {
    HesapMakinesi hesap;
    EXPECT_FALSE(hesap.asal(-5));
    EXPECT_FALSE(hesap.asal(-1));
}

Assertion'lar: EXPECT vs ASSERT

Google Test'te iki assertion ailesi var:

PrefixDavranışNe Zaman
EXPECT_*Başarısız olursa test devam ederÇoğu durumda bunu kullan
ASSERT_*Başarısız olursa test dururSonraki satırlar bu sonuca bağlıysa
TEST(VectorTest, ElemanErisimi) {
    std::vector<int> v = {10, 20, 30};
    
    // ASSERT: başarısız olursa devam etmenin anlamı yok
    ASSERT_EQ(v.size(), 3u);  // Boyut 3 değilse v[2]'ye erişmek tehlikeli
    
    // EXPECT: hepsi bağımsız kontrol
    EXPECT_EQ(v[0], 10);
    EXPECT_EQ(v[1], 20);
    EXPECT_EQ(v[2], 30);
}

Yaygın Assertion'lar

// Eşitlik
EXPECT_EQ(gercek, beklenen);        // gercek == beklenen
EXPECT_NE(a, b);                     // a != b

// Karşılaştırma
EXPECT_LT(a, b);                     // a < b
EXPECT_LE(a, b);                     // a <= b
EXPECT_GT(a, b);                     // a > b
EXPECT_GE(a, b);                     // a >= b

// Boolean
EXPECT_TRUE(ifade);
EXPECT_FALSE(ifade);

// String
EXPECT_STREQ(cstr1, cstr2);         // C-string eşitlik
EXPECT_STRNE(cstr1, cstr2);         // C-string farklılık
EXPECT_STRCASEEQ(s1, s2);           // Büyük/küçük harf duyarsız

// Floating point (kayan nokta karşılaştırma hassas)
EXPECT_FLOAT_EQ(a, b);              // ~4 ULP tolerans
EXPECT_DOUBLE_EQ(a, b);             // ~4 ULP tolerans
EXPECT_NEAR(a, b, tolerans);        // |a-b| < tolerans

// Exception
EXPECT_THROW(ifade, ExceptionTipi);  // Belirli exception fırlatmalı
EXPECT_ANY_THROW(ifade);             // Herhangi exception fırlatmalı
EXPECT_NO_THROW(ifade);              // Exception fırlatMAMALI

⚠️ Floating point karşılaştırmada `EXPECT_EQ` kullanma! Kayan nokta aritmetiği yüzünden 0.1 + 0.2 != 0.3 olabilir. Her zaman EXPECT_DOUBLE_EQ veya EXPECT_NEAR kullan.


Test Fixture: Ortak Kurulum (TEST_F)

Her test fonksiyonunda aynı nesneyi oluşturmak tekrar. Test fixture ile ortak kurulum bir kez yazılır, her test için otomatik çalıştırılır. Java'daki @BeforeEach / @AfterEach karşılığıdır.

#include <gtest/gtest.h>
#include <vector>
#include <string>
#include <algorithm>

// Test edilecek sınıf
class Envanter {
public:
    void urunEkle(const std::string& isim, int miktar) {
        urunler_.push_back({isim, miktar});
    }
    
    int toplamUrun() const { return static_cast<int>(urunler_.size()); }
    
    int stokBul(const std::string& isim) const {
        for (const auto& u : urunler_) {
            if (u.isim == isim) return u.miktar;
        }
        return -1;  // bulunamadı
    }
    
    bool urunSil(const std::string& isim) {
        auto it = std::remove_if(urunler_.begin(), urunler_.end(),
            [&](const auto& u) { return u.isim == isim; });
        if (it == urunler_.end()) return false;
        urunler_.erase(it, urunler_.end());
        return true;
    }

private:
    struct Urun { std::string isim; int miktar; };
    std::vector<Urun> urunler_;
};

// Fixture sınıfı — ::testing::Test'ten türer
class EnvanterTest : public ::testing::Test {
protected:
    // Her testten ÖNCE çalışır (Java @BeforeEach)
    void SetUp() override {
        envanter.urunEkle("Laptop", 50);
        envanter.urunEkle("Mouse", 200);
        envanter.urunEkle("Klavye", 150);
    }
    
    // Her testten SONRA çalışır (Java @AfterEach)
    void TearDown() override {
        // Gerekli temizlik (bu örnekte yok)
    }
    
    Envanter envanter;  // Her test taze bir kopya alır
};

// TEST_F kullan — F = Fixture
TEST_F(EnvanterTest, BaslangictaUcUrunVar) {
    EXPECT_EQ(envanter.toplamUrun(), 3);
}

TEST_F(EnvanterTest, StokSorgulama) {
    EXPECT_EQ(envanter.stokBul("Laptop"), 50);
    EXPECT_EQ(envanter.stokBul("Mouse"), 200);
    EXPECT_EQ(envanter.stokBul("Tablet"), -1);  // yok
}

TEST_F(EnvanterTest, UrunSilme) {
    ASSERT_TRUE(envanter.urunSil("Mouse"));
    EXPECT_EQ(envanter.toplamUrun(), 2);
    EXPECT_EQ(envanter.stokBul("Mouse"), -1);  // artık yok
}

TEST_F(EnvanterTest, OlmayanUrunSilme) {
    EXPECT_FALSE(envanter.urunSil("Tablet"));
    EXPECT_EQ(envanter.toplamUrun(), 3);  // değişmedi
}

TEST_F(EnvanterTest, YeniUrunEkleme) {
    envanter.urunEkle("Monitor", 30);
    EXPECT_EQ(envanter.toplamUrun(), 4);
    EXPECT_EQ(envanter.stokBul("Monitor"), 30);
}

Her TEST_F çalıştığında: yeni bir EnvanterTest nesnesi oluşturulur → SetUp() çağrılır → test çalışır → TearDown() çağrılır → nesne yıkılır. Testler birbirini asla etkilemez.


Parameterized Tests (TEST_P)

Aynı test mantığını farklı verilerle çalıştırmak istediğinde her seferinde yeni test yazmak yerine, parametrik testler kullanırsın:

#include <gtest/gtest.h>
#include <tuple>
#include <cmath>
#include <string>

// Test verisi: {girdi, beklenen sonuç}
struct AsalTestVerisi {
    int sayi;
    bool beklenen;
    std::string aciklama;
};

// Fixture: TestWithParam<ParametreTipi>
class AsalParametrikTest : public ::testing::TestWithParam<AsalTestVerisi> {
protected:
    HesapMakinesi hesap;
};

// TEST_P: parametrik test
TEST_P(AsalParametrikTest, AsalKontrol) {
    auto [sayi, beklenen, aciklama] = GetParam();
    EXPECT_EQ(hesap.asal(sayi), beklenen) 
        << "Basarisiz: " << aciklama;
}

// Test verilerini tanımla ve bağla
INSTANTIATE_TEST_SUITE_P(
    AsalSayilar,           // Instance adı
    AsalParametrikTest,     // Fixture sınıfı
    ::testing::Values(
        AsalTestVerisi{2, true, "En kucuk asal"},
        AsalTestVerisi{3, true, "Kucuk asal"},
        AsalTestVerisi{4, false, "2*2 asal degil"},
        AsalTestVerisi{17, true, "Orta asal"},
        AsalTestVerisi{0, false, "Sifir asal degil"},
        AsalTestVerisi{1, false, "Bir asal degil"},
        AsalTestVerisi{-7, false, "Negatif asal degil"},
        AsalTestVerisi{7919, true, "Buyuk asal"},
        AsalTestVerisi{7920, false, "Buyuk asal olmayan"}
    )
);

Bu 9 test vakası tek bir TEST_P ile yazıldı. Yeni vaka eklemek için sadece Values() listesine bir satır eklersin. Google Test her birini ayrı test olarak raporlar.

Daha basit durumlar için ::testing::Values ile doğrudan tuple da kullanılabilir:

class CarpimTest : public ::testing::TestWithParam<std::tuple<double, double, double>> {
protected:
    HesapMakinesi hesap;
};

TEST_P(CarpimTest, CarpimDogrulugu) {
    auto [a, b, beklenen] = GetParam();
    EXPECT_DOUBLE_EQ(hesap.carp(a, b), beklenen);
}

INSTANTIATE_TEST_SUITE_P(
    CarpimVerileri, CarpimTest,
    ::testing::Values(
        std::make_tuple(2.0, 3.0, 6.0),
        std::make_tuple(-1.0, 5.0, -5.0),
        std::make_tuple(0.0, 999.0, 0.0),
        std::make_tuple(0.1, 0.2, 0.02)
    )
);

Google Mock: Bağımlılıkları Taklit Etmek

Problem

Test etmek istediğin sınıf, bir veritabanına, ağ servisine veya dosya sistemine bağımlı. Bu bağımlılıkları gerçekten çalıştırmak testleri yavaşlatır, kırılgan yapar ve tekrarlanamaz kılar. Mock nesneler gerçek bağımlılıkların yerini alır ve beklediğin davranışı simüle eder.

Temel Kullanım

#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <memory>
#include <string>
#include <vector>

// Arayüz — bağımlılık
class VeritabaniArayuz {
public:
    virtual ~VeritabaniArayuz() = default;
    virtual bool baglan(const std::string& connStr) = 0;
    virtual std::vector<std::string> sorgula(const std::string& sql) = 0;
    virtual bool ekle(const std::string& tablo, const std::string& veri) = 0;
    virtual int satirSayisi(const std::string& tablo) = 0;
};

// Mock sınıfı — MOCK_METHOD ile
class MockVeritabani : public VeritabaniArayuz {
public:
    MOCK_METHOD(bool, baglan, (const std::string& connStr), (override));
    MOCK_METHOD(std::vector<std::string>, sorgula, (const std::string& sql), (override));
    MOCK_METHOD(bool, ekle, (const std::string& tablo, const std::string& veri), (override));
    MOCK_METHOD(int, satirSayisi, (const std::string& tablo), (override));
};

// Test edilecek sınıf — veritabanına bağımlı
class KullaniciServisi {
public:
    explicit KullaniciServisi(std::shared_ptr<VeritabaniArayuz> db) 
        : db_(std::move(db)) {}
    
    bool kullaniciEkle(const std::string& isim) {
        if (!db_->baglan("production_db")) return false;
        return db_->ekle("kullanicilar", isim);
    }
    
    std::vector<std::string> tumKullanicilar() {
        db_->baglan("production_db");
        return db_->sorgula("SELECT * FROM kullanicilar");
    }
    
    int kullaniciSayisi() {
        return db_->satirSayisi("kullanicilar");
    }

private:
    std::shared_ptr<VeritabaniArayuz> db_;
};

// === TESTLER ===
using ::testing::Return;
using ::testing::_;
using ::testing::HasSubstr;
using ::testing::AtLeast;

class KullaniciServisiTest : public ::testing::Test {
protected:
    void SetUp() override {
        mockDb = std::make_shared<MockVeritabani>();
        servis = std::make_unique<KullaniciServisi>(mockDb);
    }
    
    std::shared_ptr<MockVeritabani> mockDb;
    std::unique_ptr<KullaniciServisi> servis;
};

TEST_F(KullaniciServisiTest, KullaniciEklemeBaşarili) {
    // Beklentileri ayarla: mock ne yapacak?
    EXPECT_CALL(*mockDb, baglan("production_db"))
        .WillOnce(Return(true));
    
    EXPECT_CALL(*mockDb, ekle("kullanicilar", "Tolga"))
        .WillOnce(Return(true));
    
    // Test et
    EXPECT_TRUE(servis->kullaniciEkle("Tolga"));
}

TEST_F(KullaniciServisiTest, BaglantiBasarisizOlursa) {
    EXPECT_CALL(*mockDb, baglan(_))   // herhangi parametre
        .WillOnce(Return(false));
    
    // baglan false dönerse ekle çağrılMAMALI
    EXPECT_CALL(*mockDb, ekle(_, _)).Times(0);
    
    EXPECT_FALSE(servis->kullaniciEkle("Tolga"));
}

TEST_F(KullaniciServisiTest, TumKullanicilariGetir) {
    std::vector<std::string> beklenen = {"Tolga", "Ayse", "Mehmet"};
    
    EXPECT_CALL(*mockDb, baglan(_))
        .WillOnce(Return(true));
    
    EXPECT_CALL(*mockDb, sorgula(HasSubstr("SELECT")))
        .WillOnce(Return(beklenen));
    
    auto sonuc = servis->tumKullanicilar();
    ASSERT_EQ(sonuc.size(), 3u);
    EXPECT_EQ(sonuc[0], "Tolga");
}

TEST_F(KullaniciServisiTest, KullaniciSayisi) {
    EXPECT_CALL(*mockDb, satirSayisi("kullanicilar"))
        .WillOnce(Return(42));
    
    EXPECT_EQ(servis->kullaniciSayisi(), 42);
}

EXPECT_CALL Detayları

// Kaç kez çağrılacağını belirle
EXPECT_CALL(mock, fonksiyon(_))
    .Times(3);                    // Tam 3 kez
    
EXPECT_CALL(mock, fonksiyon(_))
    .Times(AtLeast(1));           // En az 1 kez
    
EXPECT_CALL(mock, fonksiyon(_))
    .Times(AtMost(5));            // En fazla 5 kez

// Farklı çağrılarda farklı sonuç
EXPECT_CALL(mock, baglan(_))
    .WillOnce(Return(false))      // İlk çağrıda false
    .WillOnce(Return(true))       // İkinci çağrıda true
    .WillRepeatedly(Return(true)); // Sonrası hep true

// Varsayılan davranış (beklenti olmadan)
ON_CALL(mock, satirSayisi(_))
    .WillByDefault(Return(0));

Yaygın Matcher'lar

using namespace ::testing;

_                    // Herhangi değer
Eq(5)               // == 5
Ne(0)               // != 0
Lt(10)              // < 10
Gt(0)               // > 0
HasSubstr("SELECT") // String içinde arama
StartsWith("http")  // String başlangıcı
IsEmpty()           // Boş container
SizeIs(3)           // Container boyutu
Contains("admin")   // Container eleman içerir

💡 Mock kullanmanın anahtarı: Test edilecek sınıfın bağımlılıklarını arayüz (interface) üzerinden almasını sağla. Constructor injection ile bağımlılığı dışarıdan ver. Bu sayede test sırasında mock, production'da gerçek implementasyonu kullanırsın.


Test Organizasyonu

İsimlendirme Kuralları

İyi test isimleri, testin ne yaptığını okumadan anlatır:

// ❌ Kötü isimler
TEST(Test1, Test_A) { ... }
TEST(Hesap, Bol1) { ... }

// ✅ İyi isimler — TestSuite_Senaryo formatı
TEST(BolmeTest, SifiraBolmeExceptionFirlatir) { ... }
TEST(BolmeTest, PozitifSayilariDogurBoler) { ... }
TEST(BolmeTest, NegatifSayilarlaCalısir) { ... }

// ✅ Alternatif: Should/When formatı
TEST(KullaniciServisi, EklemeBasariliOldugunddaTrueDoner) { ... }
TEST(KullaniciServisi, BaglantiKoptuğundaFalseDoner) { ... }

Dosya Yapısı

tests/
├── CMakeLists.txt
├── hesap_makinesi_test.cpp    // HesapMakinesi sınıfının testleri
├── envanter_test.cpp           // Envanter sınıfının testleri
├── kullanici_servisi_test.cpp  // KullaniciServisi + mock
├── integration/
│   └── veritabani_test.cpp     // Gerçek DB ile entegrasyon testleri
└── fixtures/
    └── test_data.json          // Test verileri

Her kaynak dosya için bir test dosyası. Test dosyası adı _test.cpp ile biter. Entegrasyon testleri ayrı klasörde.

Test Filtreleme

# Sadece belirli testleri çalıştır
./testler --gtest_filter="BolmeTest.*"           # BolmeTest suite'indeki tüm testler
./testler --gtest_filter="*Exception*"           # Adında Exception geçenler
./testler --gtest_filter="AsalTest.*:BolmeTest.*" # İki suite birden
./testler --gtest_filter="-*Yavas*"              # Yavaş testleri hariç tut

# Verbose çıktı
./testler --gtest_print_time=1

# Testleri rastgele sırada çalıştır (sıra bağımlılığı yakala)
./testler --gtest_shuffle

# Başarısız testleri tekrar çalıştır
./testler --gtest_repeat=3 --gtest_break_on_failure

CI/CD'de Test Çalıştırma: CTest

CMake'in test çalıştırıcısı CTest, Google Test ile sorunsuz entegre olur:

# Build sonrası
cd build
ctest                              # Tüm testleri çalıştır
ctest --output-on-failure          # Sadece başarısız testlerin çıktısını göster
ctest -j$(nproc)                   # Paralel çalıştır
ctest -R "BolmeTest"               # Regex ile filtrele
ctest --test-dir build             # Build dizinini belirt

GitHub Actions Örneği

# .github/workflows/test.yml
name: C++ Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure
        run: cmake -B build -DCMAKE_BUILD_TYPE=Release
      
      - name: Build
        run: cmake --build build -j$(nproc)
      
      - name: Test
        run: ctest --test-dir build --output-on-failure -j$(nproc)

Her push'ta ve pull request'te testler otomatik çalışır. Başarısız test varsa CI pipeline kırmızı olur — merge edilmez.


Gerçek Dünya Örneği: Banka Hesabı Test Suite

Birden fazla test tekniğini bir arada kullanan bütünleşik bir örnek:

#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <memory>
#include <string>
#include <stdexcept>

// === Arayüzler ===
class BildirimServisi {
public:
    virtual ~BildirimServisi() = default;
    virtual void bildirimGonder(const std::string& mesaj) = 0;
};

class DenetimLogu {
public:
    virtual ~DenetimLogu() = default;
    virtual void islemKaydet(const std::string& tur, double miktar) = 0;
};

// === Ana Sınıf ===
class BankaHesabi {
public:
    BankaHesabi(std::string sahip, double bakiye,
                std::shared_ptr<BildirimServisi> bildirim,
                std::shared_ptr<DenetimLogu> denetim)
        : sahip_(std::move(sahip)), bakiye_(bakiye)
        , bildirim_(std::move(bildirim)), denetim_(std::move(denetim)) {}
    
    void paraYatir(double miktar) {
        if (miktar <= 0) throw std::invalid_argument("Miktar pozitif olmali");
        bakiye_ += miktar;
        denetim_->islemKaydet("YATIRMA", miktar);
        if (miktar >= 10000) {
            bildirim_->bildirimGonder("Buyuk yatirma: " + std::to_string(miktar));
        }
    }
    
    void paraCek(double miktar) {
        if (miktar <= 0) throw std::invalid_argument("Miktar pozitif olmali");
        if (miktar > bakiye_) throw std::runtime_error("Yetersiz bakiye");
        bakiye_ -= miktar;
        denetim_->islemKaydet("CEKME", miktar);
    }
    
    double bakiye() const { return bakiye_; }
    const std::string& sahip() const { return sahip_; }

private:
    std::string sahip_;
    double bakiye_;
    std::shared_ptr<BildirimServisi> bildirim_;
    std::shared_ptr<DenetimLogu> denetim_;
};

// === Mock'lar ===
class MockBildirim : public BildirimServisi {
public:
    MOCK_METHOD(void, bildirimGonder, (const std::string&), (override));
};

class MockDenetim : public DenetimLogu {
public:
    MOCK_METHOD(void, islemKaydet, (const std::string&, double), (override));
};

// === Fixture ===
class BankaHesabiTest : public ::testing::Test {
protected:
    void SetUp() override {
        mockBildirim = std::make_shared<MockBildirim>();
        mockDenetim = std::make_shared<MockDenetim>();
        hesap = std::make_unique<BankaHesabi>(
            "Tolga", 1000.0, mockBildirim, mockDenetim);
    }
    
    std::shared_ptr<MockBildirim> mockBildirim;
    std::shared_ptr<MockDenetim> mockDenetim;
    std::unique_ptr<BankaHesabi> hesap;
};

// === Testler ===
using ::testing::_;
using ::testing::HasSubstr;

TEST_F(BankaHesabiTest, ParaYatirmaBakiyeArtirir) {
    EXPECT_CALL(*mockDenetim, islemKaydet("YATIRMA", 500.0));
    
    hesap->paraYatir(500.0);
    EXPECT_DOUBLE_EQ(hesap->bakiye(), 1500.0);
}

TEST_F(BankaHesabiTest, BuyukYatirmaBildirimGonderir) {
    EXPECT_CALL(*mockDenetim, islemKaydet(_, _));
    EXPECT_CALL(*mockBildirim, bildirimGonder(HasSubstr("Buyuk yatirma")));
    
    hesap->paraYatir(15000.0);
}

TEST_F(BankaHesabiTest, KucukYatirmaBildirimGondermez) {
    EXPECT_CALL(*mockDenetim, islemKaydet(_, _));
    EXPECT_CALL(*mockBildirim, bildirimGonder(_)).Times(0);  // ÇAĞRILMAMALI
    
    hesap->paraYatir(500.0);
}

TEST_F(BankaHesabiTest, YetersizBakiyeException) {
    EXPECT_THROW(hesap->paraCek(5000.0), std::runtime_error);
    EXPECT_DOUBLE_EQ(hesap->bakiye(), 1000.0);  // bakiye değişmedi
}

TEST_F(BankaHesabiTest, NegatifMiktarRedEder) {
    EXPECT_THROW(hesap->paraYatir(-100.0), std::invalid_argument);
    EXPECT_THROW(hesap->paraCek(0.0), std::invalid_argument);
}

TEST_F(BankaHesabiTest, ParaCekmeBakiyeDusurur) {
    EXPECT_CALL(*mockDenetim, islemKaydet("CEKME", 300.0));
    
    hesap->paraCek(300.0);
    EXPECT_DOUBLE_EQ(hesap->bakiye(), 700.0);
}

Bu örnekte:

  • TEST_F ile fixture kullanılıyor (ortak SetUp)

  • Mock nesnelerle gerçek servisler simüle ediliyor

  • EXPECT_CALL ile fonksiyonların doğru parametrelerle çağrıldığı doğrulanıyor

  • EXPECT_THROW ile exception davranışı test ediliyor

  • Her test bağımsız, tekrarlanabilir ve hızlı


Yaygın Hatalar

  1. Test sırası bağımlılığı: Testler birbirinin durumuna bağımlı olmamalı. Her test taze bir fixture almalı.

  2. Aşırı mock'lama: Her şeyi mock'lama. Sadece dış bağımlılıkları (DB, ağ, dosya sistemi) mock'la. İç sınıfları mock'lamak testleri kırılgan yapar.

  3. Implementation testi: Fonksiyonun *ne* yaptığını test et, *nasıl* yaptığını değil. İç detayı test edersen, refactor'da testler kırılır.

  4. Tek assertion per test zorunluluğu: Bir testte birden fazla assertion olabilir — ama tek bir davranışı test etmeli. "Para yatırma" testinde hem bakiye kontrolü hem denetim kaydı kontrol edilebilir.


Özet

  • Unit testing kodun doğruluğunu otomatik olarak doğrular. Elle test etmek ölçeklenmez — testler senin için kontrol eder.

  • Google Test C++'ın en yaygın test framework'üdür. CMake + FetchContent ile kolayca entegre olur.

  • TEST makrosu basit testler, TEST_F fixture ile ortak kurulum, TEST_P parametrik testler için kullanılır.

  • EXPECT_* başarısızlıkta devam eder (çoğu durumda bunu kullan), ASSERT_* durur (sonraki satırlar buna bağlıysa).

  • Google Mock ile bağımlılıkları taklit edersin. MOCK_METHOD ile mock sınıf, EXPECT_CALL ile beklenti, matcher'larla parametre kontrolü.

  • CTest ile CI/CD pipeline'ında testleri otomatik çalıştırırsın. Her commit'te testler geçmezse merge yasak.

  • Test yazılabilir kod = iyi tasarlanmış kod. Interface'ler üzerinden bağımlılık al, constructor injection kullan.