Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Test Organizasyonu

Bölümün başında belirttiğimiz gibi test yazmak karmaşık bir disiplindir ve farklı geliştiriciler farklı terminolojiler ile organizasyon biçimleri kullanır. Rust topluluğu testleri iki ana kategoride ele alır: birim testleri (unit tests) ve entegrasyon testleri (integration tests). Birim testleri daha küçük ve odaklıdır; her seferinde tek bir modülü yalıtılmış biçimde sınar ve gizli (private) arayüzleri de test edebilir. Entegrasyon testleri ise tamamen kütüphanenizin dışındadır; kodunuzu tıpkı başka bir harici kodun kullanacağı biçimde, yalnızca açık (public) arayüz üzerinden kullanır ve tek bir testte birden fazla modülü bir arada çalıştırabilir.

Kütüphanenizi oluşturan parçaların hem tek başına hem de bir arada beklediğiniz gibi çalıştığından emin olmak için her iki test türünü de yazmak önemlidir.

Birim Testleri

Birim testlerinin amacı, kodun hangi kısımlarının beklendiği gibi çalışıp hangilerinin çalışmadığını hızla tespit edebilmek için her bir kod birimini geri kalan koddan yalıtılmış olarak test etmektir. Birim testlerini, src dizini altındaki her dosyanın içine, test ettikleri kodla aynı dosyaya yerleştirirsiniz. Yerleşik kural, her dosyada test fonksiyonlarını barındıracak tests adında bir modül oluşturmak ve bu modülü cfg(test) ile işaretlemektir.

tests Modülü ve #[cfg(test)]

tests modülü üzerindeki #[cfg(test)] özniteliği Rust’a test kodunu cargo build çalıştırdığınızda değil, yalnızca cargo test çalıştırdığınızda derlemesini ve çalıştırmasını söyler. Bu sayede yalnızca kütüphaneyi derlemek istediğinizde derleme süresinden kazanır, ayrıca testler dahil edilmediği için üretilen derlenmiş çıktıda yer tasarrufu sağlarsınız. Birazdan göreceğiniz üzere entegrasyon testleri farklı bir dizinde yer aldığından #[cfg(test)] özniteliğine ihtiyaç duymaz. Ancak birim testleri asıl kodla aynı dosyalarda bulunduğundan, derlenmiş sonuca dahil edilmemeleri gerektiğini belirtmek için #[cfg(test)] kullanırsınız.

Bu bölümün ilk kısmında yeni toplayici projesini oluşturduğumuzda Cargo’nun bizim için şu kodu ürettiğini hatırlayın:

Dosya adı: src/lib.rs

pub fn topla(sol: u64, sag: u64) -> u64 {
    sol + sag
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn calisiyor() {
        let sonuc = topla(2, 2);
        assert_eq!(sonuc, 4);
    }
}

Otomatik olarak üretilen tests modülü üzerindeki cfg özniteliği İngilizce configuration (yapılandırma) kelimesinin kısaltmasıdır ve Rust’a takip eden öğenin yalnızca belirli bir yapılandırma seçeneği sağlandığında derlemeye dahil edilmesi gerektiğini bildirir. Bu örnekte yapılandırma seçeneği, testleri derlemek ve çalıştırmak amacıyla Rust tarafından sağlanan test seçeneğidir. Cargo, cfg özniteliği sayesinde test kodumuzu yalnızca testleri cargo test komutuyla açıkça çalıştırdığımızda derler. Bu kural yalnızca #[test] ile işaretlenmiş fonksiyonlar için değil, modülün içinde yer alabilecek tüm yardımcı fonksiyonlar için de geçerlidir.

Not: #[cfg(test)] özniteliğinin src/lib.rs içindeki test modülünü normal derlemelerden nasıl dışladığını ve tests/ dizinindeki entegrasyon testlerinde neden gerekmediğini derleme hedefleri üzerinden kolayca anlayabiliriz:

  • src/lib.rs (ve src/ altındaki diğer modüller): cargo build veya cargo build --release çalıştırdığınızda modül ağacının tamamı kütüphane çıktısının bir parçası olarak derlenir. mod tests üzerine #[cfg(test)] koymayı unutursanız #[test] fonksiyonları dışarıda kalsa bile modülün kendisi, içindeki yardımcı fonksiyonlar ve use bildirimleri normal derlemeye dahil edilir; bu durum gereksiz dead_code (kullanılmayan kod) uyarılarına, ikili dosya boyutunun büyümesine ve modül içinde yalnızca [dev-dependencies] altında tanımlı bir test crate’i kullanılıyorsa cargo build sırasında derleme hatasına yol açar.
  • tests/ dizini: Cargo bu dizini özel bir test hedefi olarak tanır; içindeki dosyaları cargo build veya cargo run sırasında derleyiciye hiç göndermez, yalnızca cargo test çalıştırıldığında ayrı birer test crate’i olarak derler. Dolayısıyla tests/ altındaki dosyalarda #[cfg(test)] yazmak gereksizdir.

Gizli Fonksiyon Testleri

Test topluluğunda gizli fonksiyonların doğrudan test edilip edilmemesi gerektiği konusunda süregelen bir tartışma vardır ve diğer pek çok dil gizli fonksiyonları test etmeyi zorlaştırır ya da imkânsız kılar. Hangi test yaklaşımını benimserseniz benimseyin, Rust’ın gizlilik kuralları gizli fonksiyonları test etmenize olanak tanır. Gizli ic_toplayici fonksiyonunu içeren Liste 11-12’deki koda göz atalım.

Dosya adı: src/lib.rs
pub fn iki_ekle(a: u64) -> u64 {
    ic_toplayici(a, 2)
}

fn ic_toplayici(sol: u64, sag: u64) -> u64 {
    sol + sag
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ic() {
        let sonuc = ic_toplayici(2, 2);
        assert_eq!(sonuc, 4);
    }
}
Liste 11-12: Gizli bir fonksiyonu test etme

ic_toplayici fonksiyonunun pub olarak işaretlenmediğine dikkat edin. Testler yalnızca sıradan Rust kodudur ve tests modülü de sıradan bir modüldür. “Modül Ağacındaki Bir Öğeye Başvurmak İçin Yollar” bölümünde ele aldığımız gibi, alt modüllerdeki öğeler ata modüllerindeki öğeleri kullanabilir. Bu testte tests modülünün üst modülüne ait tüm öğeleri use super::* ile kapsama dahil ediyoruz; böylece test fonksiyonumuz ic_toplayici fonksiyonunu doğrudan çağırabiliyor. Gizli fonksiyonların test edilmemesi gerektiğini düşünüyorsanız, Rust’ta sizi bunu yapmaya zorlayan hiçbir kural yoktur.

Entegrasyon Testleri

Rust’ta entegrasyon testleri kütüphanenizin tamamen dışındadır. Kütüphanenizi tıpkı diğer harici kodların kullanacağı biçimde kullanırlar; dolayısıyla kütüphanenizin yalnızca açık API’sinin parçası olan fonksiyonları çağırabilirler. Amaçları, kütüphanenizin birden fazla parçasının bir arada doğru çalışıp çalışmadığını sınamaktır. Tek başına doğru çalışan kod birimleri bir araya getirildiğinde sorun çıkarabilir; bu nedenle entegre edilmiş kodun da test kapsamına alınması önemlidir. Entegrasyon testleri oluşturmak için öncelikle bir tests dizinine ihtiyacınız vardır.

tests Dizini

Proje dizinimizin en üst seviyesinde, src dizininin hemen yanında bir tests dizini oluştururuz. Cargo, entegrasyon testi dosyalarını bu dizinde araması gerektiğini bilir. Ardından bu dizinde istediğimiz kadar test dosyası oluşturabiliriz; Cargo bu dosyaların her birini ayrı bir crate olarak derleyecektir.

Şimdi bir entegrasyon testi oluşturalım. Liste 11-12’deki kod src/lib.rs dosyasında dururken bir tests dizini oluşturun ve içine tests/entegrasyon_testi.rs adında yeni bir dosya ekleyin. Dizin yapınız şu şekilde görünmelidir:

toplayici
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── entegrasyon_testi.rs

tests/entegrasyon_testi.rs dosyasına Liste 11-13’teki kodu girin.

Dosya adı: tests/entegrasyon_testi.rs
use toplayici::iki_ekle;

#[test]
fn iki_ekliyor() {
    let sonuc = iki_ekle(2);
    assert_eq!(sonuc, 4);
}
Liste 11-13: toplayici crate’indeki bir fonksiyonun entegrasyon testi

tests dizinindeki her dosya ayrı bir crate olduğu için kütüphanemizi her bir test crate’inin kapsamına açıkça dahil etmemiz gerekir. Bu yüzden kodun en üstüne, birim testlerinde ihtiyaç duymadığımız use toplayici::iki_ekle; satırını ekliyoruz.

tests/entegrasyon_testi.rs dosyasındaki herhangi bir kodu #[cfg(test)] ile işaretlememize gerek yoktur. Cargo tests dizinini özel olarak ele alır ve bu dizindeki dosyaları yalnızca cargo test çalıştırdığımızda derler. Şimdi cargo test komutunu çalıştıralım:

$ cargo test
   Compiling toplayici v0.1.0 (file:///projects/toplayici)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running unittests src/lib.rs (target/debug/deps/toplayici-ac13090f6eeacc92)

running 1 test
test tests::ic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/entegrasyon_testi.rs (target/debug/deps/entegrasyon_testi-5737c6970c195272)

running 1 test
test iki_ekliyor ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests toplayici

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Çıktıdaki üç bölüm sırasıyla birim testlerini, entegrasyon testini ve belgelendirme testlerini içerir. Herhangi bir bölümdeki bir test başarısız olursa izleyen bölümlerin çalıştırılmayacağına dikkat edin. Örneğin bir birim testi başarısız olursa entegrasyon ve belgelendirme testleri için hiçbir çıktı üretilmez; çünkü bu testler yalnızca tüm birim testleri geçtiğinde çalıştırılır.

Birim testlerine ayrılan ilk bölüm şimdiye kadar gördüğümüzle aynıdır: her birim testi için bir satır (Liste 11-12’de eklediğimiz ic adlı test) ve ardından birim testleri için bir özet satırı yer alır.

Entegrasyon testleri bölümü Running tests/entegrasyon_testi.rs satırıyla başlar. Hemen ardından bu entegrasyon testi dosyasındaki her test fonksiyonu için bir satır ve Doc-tests toplayici bölümü başlamadan hemen önce entegrasyon testi sonuçlarına ait özet satırı gelir.

Her entegrasyon testi dosyasının çıktıda kendine ait bir bölümü vardır; dolayısıyla tests dizinine daha fazla dosya eklersek çıktıda daha fazla entegrasyon testi bölümü görürüz.

Not: Özetlemek gerekirse cargo test komutu testleri şu üç aşamalı sırayla yürütür:

  1. Birim testleri (Running unittests src/lib.rs ve varsa src/main.rs): Modüllerde tanımlanan testleri, gizli (private) öğelere de erişebilecekleri bağlamda çalıştırır. Testler kendiliğinden yalıtılmaz; paylaşılan dosya ve küresel durum gibi kaynakları test kodunun yönetmesi gerekir.
  2. Entegrasyon testleri (Running tests/<dosya_adı>.rs): tests/ dizininin kökündeki her .rs dosyasını ayrı bir harici crate olarak derleyip yalnızca açık (pub) API üzerinden sınar.
  3. Belgelendirme testleri (Doc-tests <crate_adı>): API belgelendirme yorumlarındaki (///) kod örneklerini derleyip çalıştırarak belgelerin kodla uyumlu kaldığını doğrular.

Cargo varsayılan olarak erken durdurma (fail-fast) davranışı uygular: Bir aşamada hata çıkarsa sonraki aşamaları çalıştırmaz. Birim testlerinde hata olsa bile tüm entegrasyon ve belgelendirme testlerinin de çalıştırılmasını isterseniz cargo test --no-fail-fast komutunu kullanabilirsiniz.

Belirli bir entegrasyon testi fonksiyonunu, fonksiyonun adını cargo test komutuna argüman olarak geçirerek yine tek başına çalıştırabiliriz. Belirli bir entegrasyon testi dosyasındaki tüm testleri çalıştırmak içinse cargo test komutunun --test argümanını ve ardından (.rs uzantısı olmadan) dosyanın adını kullanabilirsiniz:

$ cargo test --test entegrasyon_testi
   Compiling toplayici v0.1.0 (file:///projects/toplayici)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.12s
     Running tests/entegrasyon_testi.rs (target/debug/deps/entegrasyon_testi-5737c6970c195272)

running 1 test
test iki_ekliyor ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Bu komut yalnızca tests/entegrasyon_testi.rs dosyasındaki testleri çalıştırır.

Entegrasyon Testlerinde Alt Modüller

Entegrasyon testlerinizin sayısı arttıkça bunları düzenli tutmak için tests dizininde birden fazla dosya oluşturmak isteyebilirsiniz; örneğin test fonksiyonlarını sınadıkları işlevselliğe göre gruplandırabilirsiniz. Daha önce belirttiğimiz gibi tests dizinindeki her dosya kendi başına ayrı bir crate olarak derlenir; bu da son kullanıcıların crate’inizi kullanma biçimini daha gerçekçi şekilde taklit eden ayrı kapsamlar oluşturmak açısından oldukça faydalıdır. Ancak bu, tests dizinindeki dosyaların, Bölüm 7’de kodu modüllere ve dosyalara ayırırken öğrendiğiniz src dizini davranışını paylaşmadığı anlamına gelir.

tests dizinindeki dosyaların bu farklı davranışı, en çok birden fazla entegrasyon testi dosyasında kullanmak istediğiniz bir grup yardımcı fonksiyonunuz olduğunda ve bunları ortak bir modüle çıkarmak için Bölüm 7’deki “Modülleri Farklı Dosyalara Ayırma” adımlarını izlemeye çalıştığınızda göze çarpar. Örneğin (Rust projelerinde genellikle tests/common.rs olarak adlandırılan) tests/ortak.rs adında bir dosya oluşturup içine kurulum adlı bir fonksiyon yerleştirirsek, birden fazla test dosyasındaki çeşitli test fonksiyonlarından çağırmak istediğimiz hazırlık kodlarını kurulum içine ekleyebiliriz:

Dosya adı: tests/ortak.rs

pub fn kurulum() {
    // kütüphanenizin testlerine özgü kurulum kodu buraya gelecek
}

Testleri yeniden çalıştırdığımızda, ortak.rs dosyası hiçbir test fonksiyonu içermemesine ve kurulum fonksiyonunu henüz hiçbir yerden çağırmamamıza rağmen test çıktısında bu dosya için yeni bir bölüm açıldığını görürüz:

$ cargo test
   Compiling toplayici v0.1.0 (file:///projects/toplayici)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.14s
     Running unittests src/lib.rs (target/debug/deps/toplayici-ac13090f6eeacc92)

running 1 test
test tests::ic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/ortak.rs (target/debug/deps/ortak-65e36ecf6e3af4a8)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/entegrasyon_testi.rs (target/debug/deps/entegrasyon_testi-5737c6970c195272)

running 1 test
test iki_ekliyor ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests toplayici

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Test sonuçlarında ortak için running 0 tests satırının görüntülenmesi istediğimiz bir durum değildir; amacımız yalnızca diğer entegrasyon testi dosyalarıyla ortak kod paylaşmaktı. Test çıktısında ortak bölümünün görünmesini önlemek için tests/ortak.rs oluşturmak yerine tests/ortak/mod.rs dosyasını oluşturacağız. Proje dizinimiz artık şu şekilde görünür:

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── ortak
    │   └── mod.rs
    └── entegrasyon_testi.rs

Bu yapı, Bölüm 7’deki “Alternatif Dosya Yolları” kısmında değindiğimiz ve Rust’ın desteklemeye devam ettiği eski adlandırma kuralıdır. Dosyayı bu şekilde adlandırmak Rust’a ortak modülünü bağımsız bir entegrasyon testi dosyası olarak ele almamasını söyler. kurulum fonksiyonunun kodunu tests/ortak/mod.rs dosyasına taşıyıp tests/ortak.rs dosyasını sildiğimizde test çıktısındaki bu bölüm artık görünmez. tests dizininin alt dizinlerinde yer alan dosyalar ayrı crate’ler olarak derlenmez ve test çıktısında kendilerine ait bir bölüm oluşturmaz.

tests/ortak/mod.rs dosyasını oluşturduktan sonra onu herhangi bir entegrasyon testi dosyasından bir modül olarak kullanabiliriz. Aşağıda, tests/entegrasyon_testi.rs dosyasındaki iki_ekliyor testinden kurulum fonksiyonunun çağrılmasına dair örnek yer almaktadır:

Dosya adı: tests/entegrasyon_testi.rs

use toplayici::iki_ekle;

mod ortak;

#[test]
fn iki_ekliyor() {
    ortak::kurulum();

    let sonuc = iki_ekle(2);
    assert_eq!(sonuc, 4);
}

mod ortak; bildiriminin Liste 7-21’de gösterdiğimiz modül bildirimiyle birebir aynı olduğuna dikkat edin. Ardından test fonksiyonunun içinde ortak::kurulum() fonksiyonunu çağırabiliriz.

İkili Crate’ler İçin Entegrasyon Testleri

Projemiz yalnızca bir src/main.rs dosyası içeren ve src/lib.rs dosyası bulunmayan bir ikili crate ise, tests dizininde entegrasyon testleri oluşturup src/main.rs dosyasında tanımlanan fonksiyonları use deyimiyle kapsama dahil edemeyiz. Yalnızca kütüphane crate’leri diğer crate’lerin kullanabileceği fonksiyonları dışarıya açar; ikili crate’ler ise doğrudan tek başlarına çalıştırılmak üzere tasarlanmıştır. (Bu kısıtlama yalnızca entegrasyon testleri için geçerlidir: src/main.rs dosyasının içine #[cfg(test)] mod tests biçiminde birim testleri yazmaya devam edebilirsiniz; cargo test çıktısındaki Running unittests src/main.rs satırı bu testlere aittir.)

Bir ikili dosya üreten Rust projelerinin, src/lib.rs dosyasında yer alan asıl iş mantığını çağıran yalın bir src/main.rs dosyasına sahip olmasının başlıca nedenlerinden biri budur. Bu mimari kullanıldığında entegrasyon testleri, kütüphane crate’ini use ile kapsama dahil ederek temel işlevselliği test edebilir. Temel işlevsellik doğru çalışıyorsa src/main.rs dosyasındaki az miktardaki kod da sorunsuz çalışacaktır ve bu küçük kod parçasının ayrıca test edilmesine gerek kalmaz.

Özet

Rust’ın test özellikleri, kodunuzda değişiklikler yapsanız bile beklediğiniz şekilde çalışmaya devam etmesini güvence altına almak için kodun nasıl davranması gerektiğini tanımlamanızı sağlar. Birim testleri bir kütüphanenin farklı parçalarını ayrı ayrı sınar ve gizli uygulama ayrıntılarını test edebilir. Entegrasyon testleri ise kütüphanenin pek çok parçasının bir arada doğru çalışıp çalışmadığını denetler ve kodu tıpkı harici bir kodun kullanacağı biçimde, kütüphanenin açık API’si üzerinden test eder. Rust’ın tür sistemi ve sahiplik kuralları pek çok hata türünü henüz derleme aşamasında önlemeye yardımcı olsa da, kodunuzdan beklenen davranışla ilgili mantık hatalarını azaltmak için testler büyük önem taşır.

Şimdi bu bölümde ve önceki bölümlerde öğrendiklerinizi bir araya getirerek gerçek bir proje üzerinde çalışalım!