00 Dile gömülü test
C/C++'ta test ayrı bir projedir: gtest'i indir, derle, linkle; CTest'i CMake'e bağla; ayrı bir test target tanımla. Rust'ta test dilin ve cargo'nun parçası. Kurulum sıfır, link derdi yok.
C/C++'taki ritüel
Tipik bir gtest kurulumu şuna benzer: bağımlılığı (vendored ya da FetchContent) projeye çek, test ikilisi için ayrı bir hedef yaz, gtest_main ile linkle, sonra CTest'e kaydet. Test kodu üretim kodundan fiziksel olarak ayrı dosyalarda durur; private fonksiyonlara erişmek için ya friend hileleri ya da başlık dosyası sızıntıları gerekir.
# gtest'i çek, ayrı test hedefi kur, linkle, CTest'e kaydet
enable_testing()
add_executable(unit_tests test_topla.cpp)
target_link_libraries(unit_tests GTest::gtest_main mylib)
add_test(NAME unit_tests COMMAND unit_tests)Rust'ta aynı şey
Rust'ta test çatısı standart araç zincirinde gelir. Bir fonksiyonun üstüne #[test] yazarsın, cargo test dersin — derleyici test ikilisini kendisi üretir, koşturur, raporlar. Ayrı hedef, ayrı bağımlılık, ayrı link adımı yok.
# sıfır kurulum — test runner cargo'nun içinde
cargo test
cargo test --release # optimize edilmiş build üzerindeC/C++ → gtest indir → ayrı target → linkle → CTest kaydet Rust → #[test] yaz → cargo test
Üç test türü, tek araç
Rust üç test türünü tek komutla yönetir; üçü de kaynak ağacının doğal parçasıdır:
#[cfg(test)] modülünde; private'a erişirtests/ dizininde, sadece public API'yi kara kutu olarak görürTest ikilisi varsayılan olarak paralel koşar (her test ayrı thread). Bu, gtest'in --gtest_shuffle gibi ek bayrak gerektirdiği davranışın Rust'taki varsayılanıdır. Paylaşılan duruma dokunan testler için cargo test -- --test-threads=1 ile seri koşturursun.
Bu bölümde
- C/C++'ta test ayrı framework, ayrı target, manuel link adımı gerektirir
- Rust'ta test runner cargo'nun parçası; kurulum ve link derdi yok
- Üç test türü tek komutla: birim, entegrasyon, doctest
- Testler varsayılan olarak paralel koşar
01 #[test] temel
Bir fonksiyonu test yapan tek şey üstündeki #[test] özniteliğidir. #[cfg(test)] ise o modülün yalnızca test derlemesinde var olmasını sağlar — üretim ikilisine tek bayt sızmaz.
En küçük örnek
Test edilecek fonksiyon ve onu kaynak yanında kontrol eden bir test modülü:
pub fn topla(a: i32, b: i32) -> i32 {
a + b
}
// Bu modül SADECE `cargo test` sırasında derlenir.
// Normal `cargo build`'de hiç var olmaz — ölü kod uyarısı da çıkmaz.
#[cfg(test)]
mod tests {
use super::*; // üst modüldeki topla'yı içeri al
#[test]
fn topla_dogru_calisir() {
assert_eq!(topla(2, 2), 4);
}
}Çıktının okunması
cargo test her testi adıyla raporlar; ok / FAILED ve özet satırı gtest çıktısına çok benzer:
cargo test
# running 1 test
# test tests::topla_dogru_calisir ... ok
# test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured#[cfg(test)] ile #[test] farklı işler yapar. #[cfg(test)] derleme koşuludur: kodu yalnızca test profilinde var eder. #[test] ise bir fonksiyonu test runner'a kaydeder. Bütün tests modülünü #[cfg(test)] ile sarmak, test yardımcılarının (mock, sabit) üretim ikilisini şişirmemesini garanti eder.
Bu bölümde
#[test]bir fonksiyonu test runner'a kaydeder#[cfg(test)]modülü yalnızca test derlemesinde var eder- Test kodu ve yardımcıları üretim ikilisine sızmaz
cargo testçıktısı test başına ok/FAILED + özet verir
02 assert makroları
Test başarısızlığı Rust'ta panic demektir. assert! ailesi koşul sağlanmazsa panic eder; panic eden test FAILED sayılır. gtest'teki EXPECT_* / ASSERT_* ikiliğinin aksine Rust'ta tek tarz vardır: ilk başarısızlık testi düşürür.
Üç temel makro
#[test]
fn assert_ornekleri() {
assert!(2 + 2 == 4); // koşul; ASSERT_TRUE gibi
assert_eq!(topla(2, 3), 5); // eşitlik; iki tarafı da yazdırır
assert_ne!(topla(2, 3), 6); // eşitsizlik
}assert_eq!, assert!(a == b)'ye yeğlenir: başarısızlıkta left/right değerlerini otomatik yazdırır, hata mesajı kendiliğinden bilgilendiricidir. Tipler PartialEq ve Debug türetmiş olmalıdır.
Özel mesaj
Her makro format! tarzı ek argüman alır; bağlam eklemek için:
#[test]
fn ozel_mesaj() {
let n = hesapla();
assert!(n > 0, "beklenen pozitif, bulunan: {}", n);
}Panic beklemek: #[should_panic]
Bazı testler kodun panic etmesini doğrular (gtest'teki EXPECT_DEATH'in hafif karşılığı). expected ile panic mesajının bir alt dizgesini şart koşarsın — yanlış sebeple panic eden kodun testi geçmesini önler:
pub fn bol(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("sifira bolme");
}
a / b
}
#[test]
#[should_panic(expected = "sifira bolme")] // mesaj eşleşmeli
fn sifira_bolme_panik() {
bol(10, 0);
}#[should_panic]'i expected olmadan kullanma. Mesajsız hâli herhangi bir panic'i kabul eder; testin kontrol etmek istediğin hata yerine alakasız bir bug yüzünden panic ederse fark edemezsin. expected dizgesini her zaman ver.
Bu bölümde
assert!,assert_eq!,assert_ne!koşul sağlanmazsa panic ederassert_eq!başarısızlıkta her iki değeri yazdırır (Debug gerekir)- Makrolar
format!tarzı özel mesaj alır #[should_panic(expected = "...")]beklenen panic'i doğrular
03 Birim testlerinin organizasyonu
Rust geleneği: birim testleri test ettikleri kodun yanında, aynı dosyada durur. #[cfg(test)] mod tests + use super::*; deseni private fonksiyonlara doğal erişim sağlar — C++'taki friend ya da başlık sızıntısı hilelerine gerek kalmaz.
Private'a erişim
Test modülü, kapsadığı modülün bir alt modülüdür; Rust'ta alt modül, üst modülün private öğelerini görür. Böylece kara kutu API'sini bozmadan iç mantığı test edebilirsin:
pub fn ayikla(girdi: &str) -> u32 {
normalize(girdi).len() as u32
}
// private — public API'nin parçası değil, ama test edilmeli
fn normalize(s: &str) -> String {
s.trim().to_lowercase()
}
#[cfg(test)]
mod tests {
use super::*; // normalize de dahil, private fonksiyonu görür
#[test]
fn normalize_trim_ve_lowercase() {
assert_eq!(normalize(" Merhaba "), "merhaba");
}
}Birim vs entegrasyon
İki konumun erişim ve amaç farkı:
| Konum | Görüş alanı | Amaç |
|---|---|---|
Kaynak içi #[cfg(test)] | private + public | İç mantık, kenar durumlar (white-box) |
tests/ dizini | sadece public | API sözleşmesi, kullanıcı bakışı (black-box) |
Testleri kod yanında tutmanın pratik faydası: bir fonksiyonu değiştirdiğinde testi göz hizasındadır, birlikte güncellersin. #[cfg(test)] sayesinde bu testler üretim ikilisine girmediği için "yanında durması" hiçbir maliyet getirmez.
Bu bölümde
- Birim testleri test ettikleri kodla aynı dosyada, alt modülde durur
use super::*;üst modülün private öğelerini içeri alır- Private fonksiyonlar friend/başlık hilesi olmadan test edilir
- Kaynak içi test = white-box;
tests/= black-box
04 Entegrasyon testleri
Proje kökündeki tests/ dizinindeki her dosya ayrı bir crate olarak derlenir ve kütüphaneyi tıpkı dışarıdan bir kullanıcı gibi, yalnızca pub API üzerinden görür. Bu, API sözleşmenin gerçekten dışarıdan kullanılabilir olduğunu doğrular.
Tipik entegrasyon dosyası
Crate'ini adıyla use edersin; private'a erişim yoktur:
// Bu dosya ayrı bir crate; kütüphaneyi dışarıdan kullanır.
use mylib::ayikla; // SADECE pub öğeler erişilebilir
#[test]
fn public_api_uctan_uca() {
assert_eq!(ayikla(" Merhaba "), 7);
}Ortak yardımcılar: tests/common/
Her test dosyası ayrı crate olduğundan, paylaşılan kurulum kodu (fixture, builder) dikkat ister. tests/common.rs kendi başına bir test crate'i olarak derlenip "0 test" raporlar — istemediğin gürültü. Çözüm, ortak kodu tests/common/mod.rs içine koymaktır; bu yol Rust tarafından test crate'i sayılmaz:
// tests/common/mod.rs — ortak fixture; ayrı test crate'i sayılmaz
pub fn ornek_girdi() -> String {
" Merhaba ".to_string()
}mod common; // tests/common/mod.rs'i modül olarak çek
use mylib::ayikla;
#[test]
fn fixture_ile() {
let g = common::ornek_girdi();
assert_eq!(ayikla(&g), 7);
}Entegrasyon testleri yalnızca kütüphane crate'leri (src/lib.rs) içindir. Saf binary crate'in (src/main.rs, lib yok) tests/ dizininden use edilecek public API'si olmaz. Pratik çözüm: mantığı lib.rs'e taşı, main.rs'i ince bir kabuk yap.
Bu bölümde
tests/içindeki her dosya ayrı crate; sadece pub API'yi görür- Entegrasyon testi API sözleşmesini dışarıdan doğrular (black-box)
- Ortak yardımcılar
tests/common/mod.rs'e konur (test crate'i sayılmaz) - Entegrasyon testleri lib crate gerektirir; mantığı lib.rs'e taşı
05 Test'te Result ve kontrol
Bir test Result<(), E> de dönebilir; bu durumda ? operatörünü testin içinde kullanır, hatayı zincirleyebilirsin. Pahalı testleri #[ignore] ile dışarıda tutar, filtre ve bayraklarla seçici koşturursun.
Result dönen test
Err dönen bir test başarısız sayılır. Bu, unwrap() yığını yerine ? kullanmanı sağlar — test kodu üretim kodu kadar temiz olur:
#[test]
fn ayrist_ve_dogrula() -> Result<(), Box<dyn std::error::Error>> {
let n: i32 = "42".parse()?; // ? — hata olursa test Err döner = FAILED
assert_eq!(n, 42);
Ok(()) // başarı için Ok(()) döndürmek zorunlu
}Result dönen testlerde #[should_panic] kullanılamaz — panic yerine Err ile başarısızlığı ifade edersin. İkisi birbirinin alternatifidir.
Pahalı testler: #[ignore]
Dakikalar süren testleri (büyük dosya, ağ) normal koşudan çıkar; CI'da açıkça iste:
#[test]
#[ignore = "yavas: 30sn suren entegrasyon"]
fn agir_is() {
// ... pahalı kurulum ...
}cargo test # ignored testler atlanir
cargo test -- --ignored # SADECE ignored olanlari kosur
cargo test -- --include-ignored # hepsini birden kosurFiltreleme ve çıktı
Test adına göre filtrele (alt dizge eşleşmesi), çıktıyı yakalamayı kapat:
cargo test ayrist # adında "ayrist" geçen testler
cargo test -- --nocapture # gecen testlerin println! ciktisini goster
cargo test -- --test-threads=1 # seri kosum (paylasimli durum)Varsayılan olarak geçen testlerin stdout'u yutulur; sadece başarısızların çıktısı gösterilir. --nocapture bu davranışı kapatır. cargo test ile runner arasındaki -- ayracı önemlidir: solu cargo'ya, sağı test ikilisine gider.
Bu bölümde
- Test
Result<(), E>dönerek?kullanabilir;Err= FAILED #[ignore]pahalı testleri normal koşudan çıkarır--ignored/--include-ignoredile seçici koştur- Ad filtresi +
--nocapture+--test-threadsile koşumu yönet
06 doctest
Dokümantasyon yorumlarındaki (///) kod blokları gerçek testlerdir: cargo test onları derler ve koşturur. Sonuç: dokümandaki örnek her zaman derlenen, çalışan, doğru koddur. C++'ta dokümantasyondaki örnek çürürken kimse fark etmez; Rust'ta bayatlayan örnek CI'ı kırar.
Temel doctest
/// içindeki ``` fence'leri bir doctest tanımlar. Örnek koddaki assert_eq! de koşturulur:
/// İki sayıyı toplar.
///
/// # Örnek
///
/// ```
/// use mylib::topla;
/// assert_eq!(topla(2, 3), 5);
/// ```
pub fn topla(a: i32, b: i32) -> i32 {
a + b
}cargo test --doc # sadece doctest'leri kosur
# test result: ok. 1 passed; 0 failedGizli satırlar ve öznitelikler
Doctest'ler dokümantasyon olduğundan okunabilir kalmalı ama yine de bağımsız derlenmeli. # ile başlayan satırlar koşar fakat render edilen dokümanda gizlenir — kurulum gürültüsünü saklarsın. Code fence'e öznitelik eklersin:
/// Dosya okur.
///
/// ```no_run
/// # use std::fs; // # ile baslayan satir render'da gizli
/// let s = fs::read_to_string("veri.txt").unwrap();
/// println!("{}", s.len()); // no_run: derlenir ama calismaz
/// ```
pub fn oku() { /* ... */ }Her doctest, görünmez bir fn main() ile sarılıp ayrı ayrı derlenir; bu yüzden gereken use ifadelerini içerir. I/O, rastgelelik ya da ağ gerektiren örneklerde no_run kullan — örnek yine derlenir (API doğru kalır) ama yan etkili kod CI'da koşmaz.
Bu bölümde
///içindeki kod blokları derlenip koşturulan doctest'lerdir- Dokümandaki örnek her zaman güncel ve doğru kalır
#ile başlayan satırlar koşar ama render'da gizlenirno_run,should_panic,compile_failöznitelikleriyle davranış ayarlanır
07 Benchmark — criterion
Stabil Rust'ta yerleşik #[bench] nightly gerektirir. Pratik çözüm criterion crate'idir: istatistiksel olarak sağlam, ölçüm gürültüsünü modelleyen, regresyon tespiti yapan bir benchmark çatısı — Google Benchmark'ın Rust dünyasındaki karşılığı.
Neden criterion?
std'nin test::Bencher'ı yıllardır #![feature(test)] arkasında kilitli; üretim projeleri stabil toolchain kullandığından erişilemez. criterion ise stabil Rust'ta koşar, her ölçümü çok sayıda örnekle alır, ortalamayı güven aralığıyla raporlar ve önceki koşuyla kıyaslayıp "%X yavaşladı/hızlandı" der.
Kurulum
Benchmark'lar benches/ dizininde durur; Cargo'ya ayrı bir [[bench]] hedefi olarak tanıtılır. harness = false, std test runner'ını devre dışı bırakıp criterion'ın kendi main'ini kullanmasını söyler:
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "topla_bench" # benches/topla_bench.rs
harness = false # std bench harness'ini kapat, criterion kendi main'ini koyarBenchmark yazımı
Kritik nokta black_box: derleyiciye değerin "opak" olduğunu söyler, böylece ölçtüğün hesabı sabit-katlama ya da ölü-kod eleme ile optimize edip silmez:
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use mylib::topla;
fn bench_topla(c: &mut Criterion) {
c.bench_function("topla 2+3", |b| {
// black_box: derleyici girdiyi sabit sayip sonucu silmesin
b.iter(|| topla(black_box(2), black_box(3)));
});
}
criterion_group!(benches, bench_topla);
criterion_main!(benches);cargo bench # criterion calistirir, raporlar
# topla 2+3 time: [812.4 ps 815.1 ps 818.3 ps]
# onceki kosuyla kiyaslar: "change: -1.2% (no regression)"Benchmark'lar her zaman --release ile koşmalı; cargo bench bunu varsayılan yapar. Debug build'de ölçüm anlamsızdır. Ayrıca black_box unutulursa optimize edici tüm hesabı silebilir ve "0 ns" gibi yanıltıcı sonuçlar görürsün.
Bu bölümde
- std
#[bench]nightly gerektirir; stabilde criterion kullanılır - criterion istatistiksel ölçüm + regresyon kıyası yapar
benches/+[[bench]]+harness = falseile kurulurblack_boxoptimize edicinin ölçülen kodu silmesini engeller
08 Property-based testing — proptest
Örnek-tabanlı testte sen birkaç girdi seçer ve beklenen çıktıyı yazarsın. Property-based testte ise bir invariant (her zaman doğru olması gereken özellik) tanımlar, çatı yüzlerce rastgele girdi üretip onu zorlar. Bir girdi invariant'ı bozarsa, proptest onu shrink ederek minimal karşı örneği bulur.
Roundtrip property
Klasik invariant: decode(encode(x)) == x. Her x için doğru olmalı; sen tek tek değer yazmak yerine özelliği bildirirsin:
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
// her i32 icin: parse(to_string(n)) == n olmali
#[test]
fn roundtrip(n in any::<i32>()) {
let s = n.to_string();
let geri: i32 = s.parse().unwrap();
prop_assert_eq!(geri, n); // proptest'in assert'i
}
}
}any::<i32>() bir stratejidir: rastgele i32 üretir. Aralık daraltmak için 0..1000i32 gibi stratejiler de yazılır.
Shrinking: minimal karşı örnek
Property-based testin asıl gücü budur. Test 7.314.829 gibi karmaşık bir girdide kırılırsa, proptest hatayı koruyan en küçük girdiyi arar — çoğu zaman 0 ya da 1 gibi okunaklı bir değere iner. C++ fuzzer'larında elle yapılan bu işi proptest otomatik yapar:
kirilan girdi: 7314829 → shrink → minimal: 256 "hata 256'da" diyen rapor + tekrar icin seed
Komşu araçlar
| Araç | Yaklaşım |
|---|---|
| proptest | Esnek stratejiler + güçlü shrinking; modern tercih |
| quickcheck | Haskell QuickCheck portu; Arbitrary trait, daha minimal API |
| cargo-fuzz | libFuzzer tabanlı coverage-güdümlü fuzzing; çökme/UB avı (genelde unsafe kod için) |
proptest girdiyi kendisi üreterek invariant'ı zorlar; cargo-fuzz ise coverage geri beslemesiyle kod yollarını sistematik gezer — özellikle parser ve unsafe blokları için. İkisi tamamlayıcıdır: property test mantıksal invariant'ı, fuzzing dayanıklılığı (panic/UB yokluğu) kovalar.
Bu bölümde
- Property-based test örnek yerine invariant bildirir; çatı rastgele girdi üretir
proptest!+ strateji (any::<T>()) +prop_assert_eq!- Shrinking ile kırılan girdi minimal karşı örneğe indirgenir
- quickcheck benzer; cargo-fuzz coverage-güdümlü fuzzing için tamamlayıcı
09 Gerçek örnek
Küçük bir fonksiyonu — bir tarih dizgesini ayrıştıran parse_gun — dört test türüyle birden kuşatalım: doctest (dokümandaki örnek), birim testi (private + kenar durumlar), entegrasyon testi (public API), ve bir proptest property (roundtrip invariant).
Fonksiyon + doctest
/// "YYYY-AA-GG" dizgesinden gün (1–31) ayıklar.
///
/// ```
/// use takvim::parse_gun;
/// assert_eq!(parse_gun("2026-06-26"), Some(26));
/// assert_eq!(parse_gun("gecersiz"), None);
/// ```
pub fn parse_gun(s: &str) -> Option<u8> {
let gun_str = s.split('-').nth(2)?; // 3. alan ya da None
let gun: u8 = gun_str.parse().ok()?;
gecerli_gun(gun).then_some(gun) // private kontrol
}
fn gecerli_gun(g: u8) -> bool {
(1..=31).contains(&g)
}Birim testi (private + kenar durum)
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn private_gecerli_gun() {
assert!(gecerli_gun(1) && gecerli_gun(31));
assert!(!gecerli_gun(0) && !gecerli_gun(32)); // sinir
}
#[test]
fn bozuk_girdi_none() {
assert_eq!(parse_gun("2026-06-99"), None); // aralik disi
assert_eq!(parse_gun("2026"), None); // eksik alan
}
// proptest: 1..=31 uretip roundtrip invariant'ini zorla
proptest! {
#[test]
fn roundtrip_gun(g in 1u8..=31) {
let s = format!("2026-06-{:02}", g);
prop_assert_eq!(parse_gun(&s), Some(g));
}
}
}Entegrasyon testi (public API)
use takvim::parse_gun; // disaridan, sadece pub
#[test]
fn public_parse_gun() {
assert_eq!(parse_gun("2026-12-01"), Some(1));
}Çıktının okunması
Tek cargo test, dört türü de ayrı bloklarda raporlar — unittests, entegrasyon ikilisi ve Doc-tests:
cargo test
# Running unittests src/lib.rs
# test tests::private_gecerli_gun ... ok
# test tests::bozuk_girdi_none ... ok
# test tests::roundtrip_gun ... ok
#
# Running tests/parse.rs
# test public_parse_gun ... ok
#
# Doc-tests takvim
# test src/lib.rs - parse_gun (line 3) ... ok
# test result: ok. 5 passed; 0 failedTek tek bakıldığında her tür ayrı bir güvence verir: doctest dokümanı dürüst tutar, birim testi private mantığı ve kenarları yakalar, entegrasyon testi API sözleşmesini doğrular, proptest beklemediğin girdileri arar. Hepsi aynı cargo test komutuyla, ek altyapı olmadan — C/C++'taki ayrı framework + ayrı target + ayrı koşturucu yığınının yerini tek satır alır.
Bu bölümde
- Dört test türü tek fonksiyonu farklı açılardan kuşatır
- Tek
cargo test: unittests, entegrasyon ve Doc-tests blokları - Her tür ayrı güvence: doküman, private mantık, API sözleşmesi, invariant
- Ayrı altyapı olmadan, dile ve cargo'ya gömülü tam test kapsaması