Nasıl Test Yazılır
Testler, test dışı kodun beklenen şekilde çalışıp çalışmadığını doğrulayan Rust fonksiyonlarıdır. Test fonksiyonlarının gövdeleri tipik olarak şu üç adımı gerçekleştirir:
- Gerekli verileri veya durumu hazırlamak.
- Test etmek istediğiniz kodu çalıştırmak.
- Sonuçların beklediğiniz gibi olduğunu doğrulamak.
Rust’ın bu adımları yerine getiren testler yazmak için özel olarak sunduğu test özniteliği (attribute), birkaç doğrulama makrosu ve should_panic özniteliği gibi olanakları inceleyelim.
Not: Yukarıdaki üç adım, yazılım mühendisliğinde Hazırla–Çalıştır–Doğrula (Arrange–Act–Assert, kısaca AAA) düzeni olarak bilinir. Birim testlerinizi bu üç aşamaya göre kurgulamak —ve Liste 11-6’da göreceğiniz gibi aşamaları gerektiğinde boş bir satırla ayırmak— her testin tek bir davranışa odaklanmasını sağlar; test başarısız olduğunda sorunun kurulumdan mı, çağrılan fonksiyondan mı yoksa beklenen sonuçtan mı kaynaklandığını bir bakışta görmenizi kolaylaştırır.
Test Fonksiyonlarını Yapılandırma
En yalın haliyle Rust’ta bir test, test özniteliğiyle işaretlenmiş bir fonksiyondur. Öznitelikler, Rust kod parçaları hakkındaki meta verilerdir; Bölüm 5’te struct’larla birlikte kullandığımız derive özniteliği buna bir örnektir. Bir fonksiyonu test fonksiyonuna dönüştürmek için fn anahtar kelimesinden önceki satıra #[test] eklemeniz yeterlidir. Testlerinizi cargo test komutuyla çalıştırdığınızda Rust, #[test] ile işaretlenmiş fonksiyonları yürüten ve her bir test fonksiyonunun geçip geçmediğini bildiren bir test çalıştırıcısı ikili dosyası derler.
Cargo ile yeni bir kütüphane projesi oluşturduğumuzda, içinde örnek bir test fonksiyonu bulunan bir test modülü bizim için otomatik olarak üretilir. Bu modül, testlerinizi yazmanız için hazır bir şablon sunar; böylece her yeni projeye başladığınızda tam yapıyı ve sözdizimini aramak zorunda kalmazsınız. İstediğiniz kadar ek test fonksiyonu ve test modülü ekleyebilirsiniz!
Gerçek bir kodu test etmeye geçmeden önce, bu şablon test üzerinde denemeler yaparak testlerin çalışma biçimine dair bazı temel noktaları keşfedeceğiz. Ardından kendi yazdığımız kodu çağıran ve davranışının doğru olduğunu sınayan gerçek dünya testleri yazacağız.
İki sayıyı toplayacak toplayici adında yeni bir kütüphane projesi oluşturalım:
$ cargo new toplayici --lib
Created library `toplayici` project
$ cd toplayici
toplayici kütüphanenizdeki src/lib.rs dosyasının içeriği (tanımlayıcılar Türkçeleştirildiğinde) Liste 11-1’deki gibi görünmelidir.
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);
}
}
cargo new tarafından otomatik olarak üretilen kodDosya, test edecek bir kodumuz olması için örnek bir topla fonksiyonuyla başlar (cargo new --lib komutu varsayılan şablonda İngilizce add ve it_works adlarını üretir; örneklerimizle uyumlu olması için burada topla ve calisiyor adlarını kullanıyoruz).
Şimdilik yalnızca calisiyor fonksiyonuna odaklanalım. #[test] özniteliğine dikkat edin: Bu öznitelik fonksiyonun bir test fonksiyonu olduğunu belirtir; böylece test çalıştırıcısı bu fonksiyonu bir test olarak ele alması gerektiğini bilir. Ortak senaryoları hazırlamak veya sık tekrarlanan işlemleri yürütmek amacıyla tests modülü içinde test olmayan yardımcı fonksiyonlar da bulundurabiliriz; bu yüzden hangi fonksiyonların test olduğunu her zaman açıkça belirtmemiz gerekir.
Örnek fonksiyon gövdesi, topla fonksiyonunu 2 ve 2 argümanlarıyla çağırmanın sonucunu tutan sonuc değişkeninin 4’e eşit olduğunu doğrulamak (assert) için assert_eq! makrosunu kullanır. Bu doğrulama ifadesi, tipik bir testin biçimine örnek teşkil eder. Testin geçtiğini görmek için çalıştıralım.
cargo test komutu, Liste 11-2’de gösterildiği gibi projemizdeki tüm testleri çalıştırır.
$ cargo test
Compiling toplayici v0.1.0 (file:///projects/toplayici)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (target/debug/deps/toplayici-01ad14159ff659ab)
running 1 test
test tests::calisiyor ... 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
Cargo testi derledi ve çalıştırdı. Çıktıda 1 testin çalıştırıldığını belirten running 1 test satırını görüyoruz. Bir sonraki satırda, üretilen test fonksiyonunun adı (tests::calisiyor) ve bu testi çalıştırma sonucunun başarılı olduğunu gösteren ok ifadesi yer alır. Genel özet satırındaki test result: ok. tüm testlerin geçtiği anlamına gelir; 1 passed; 0 failed kısmı ise geçen ve başarısız olan testlerin toplam sayısını verir.
Bir testi belirli bir çalıştırmada yürütülmemesi için yok sayılmış olarak işaretlemek mümkündür; bunu bu bölümün ilerleyen kısımlarındaki “Açıkça İstenmedikçe Testleri Yok Sayma” başlığında ele alacağız. Burada böyle bir işaretleme yapmadığımız için özette 0 ignored (0 yok sayıldı) yazar. Ayrıca cargo test komutuna bir argüman geçirerek yalnızca adı belirli bir metinle eşleşen testleri çalıştırabiliriz; buna filtreleme denir ve bu konuyu “Testlerin Bir Alt Kümesini İsme Göre Çalıştırma” başlığında inceleyeceğiz. Çalıştırılan testleri filtrelemediğimiz için özetin sonunda 0 filtered out (0 filtrelendi) ifadesi yer alır.
0 measured (0 ölçüldü) istatistiği, performansı ölçen kıyaslama (benchmark) testleri içindir. Kıyaslama testleri, bu satırların yazıldığı sırada yalnızca Rust’ın gecelik (nightly) sürümünde kullanılabilmektedir. Daha fazla bilgi edinmek için kıyaslama testleri hakkındaki belgelere göz atabilirsiniz.
Test çıktısının Doc-tests toplayici ile başlayan bir sonraki bölümü ise belgelendirme testlerinin (Doc-tests) sonuçlarına ayrılmıştır. Henüz herhangi bir belgelendirme testimiz yok; ancak Rust, API belgelendirmemizde yer alan kod örneklerini derleyip çalıştırabilir. Bu özellik, belgelerinizin kodunuzla her zaman uyumlu kalmasına yardımcı olur! Belgelendirme testlerinin nasıl yazılacağını Bölüm 14’teki “Belgelendirme Yorumlarını Test Olarak Kullanma” başlığında ele alacağız. Şimdilik Doc-tests çıktısını göz ardı edeceğiz.
Testi kendi ihtiyaçlarımıza göre özelleştirmeye başlayalım. İlk olarak calisiyor fonksiyonunun adını, örneğin aşağıdaki gibi kesif olacak şekilde değiştirin:
Dosya adı: src/lib.rs
pub fn topla(sol: u64, sag: u64) -> u64 {
sol + sag
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kesif() {
let sonuc = topla(2, 2);
assert_eq!(sonuc, 4);
}
}
Ardından cargo test komutunu tekrar çalıştırın. Çıktı artık calisiyor yerine kesif adını gösterir:
$ cargo test
Compiling toplayici v0.1.0 (file:///projects/toplayici)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
Running unittests src/lib.rs (target/debug/deps/toplayici-92948b65e88960b4)
running 1 test
test tests::kesif ... 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
Şimdi bir test daha ekleyeceğiz, ancak bu kez başarısız olan bir test yazacağız! Test fonksiyonunun içindeki bir kod paniklediğinde test başarısız olur. Her test ayrı bir thread’de (iş parçacığında) çalıştırılır ve ana thread bir test thread’inin çöktüğünü gördüğünde o testi başarısız olarak işaretler. Bölüm 9’da panik oluşturmanın en basit yolunun panic! makrosunu çağırmak olduğundan bahsetmiştik. Yeni testi baska_bir_test adında bir fonksiyon olarak ekleyin; src/lib.rs dosyanız Liste 11-3’teki gibi görünecektir.
pub fn topla(sol: u64, sag: u64) -> u64 {
sol + sag
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kesif() {
let sonuc = topla(2, 2);
assert_eq!(sonuc, 4);
}
#[test]
fn baska_bir_test() {
panic!("Bu testi başarısız yap");
}
}
panic! makrosunu çağırdığımız için başarısız olacak ikinci bir test eklemecargo test komutuyla testleri tekrar çalıştırın. Çıktı Liste 11-4’teki gibi görünmeli; kesif testimizin geçtiğini, baska_bir_test testimizin ise başarısız olduğunu göstermelidir.
$ cargo test
Compiling toplayici v0.1.0 (file:///projects/toplayici)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/toplayici-92948b65e88960b4)
running 2 tests
test tests::baska_bir_test ... FAILED
test tests::kesif ... ok
failures:
---- tests::baska_bir_test stdout ----
thread 'tests::baska_bir_test' (6019162) panicked at src/lib.rs:17:9:
Bu testi başarısız yap
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::baska_bir_test
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
test tests::baska_bir_test satırında ok yerine FAILED (başarısız) yazar. Tek tek test sonuçları ile genel özet arasında iki yeni bölüm belirir: İlk bölüm, her bir test başarısızlığının ayrıntılı nedenini gösterir. Bu örnekte tests::baska_bir_test testinin, src/lib.rs dosyasının 17. satırında Bu testi başarısız yap mesajıyla paniklediği için başarısız olduğu ayrıntısını görürüz. İkinci bölüm ise yalnızca başarısız olan tüm testlerin adlarını listeler; çok sayıda testin ve uzun hata çıktılarının bulunduğu projelerde bu liste oldukça kullanışlıdır. Başarısız olan bir testin adını kullanarak hataları daha kolay ayıklamak üzere yalnızca o testi çalıştırabiliriz; testleri çalıştırma yöntemlerini “Testlerin Nasıl Çalıştırılacağını Kontrol Etme” bölümünde daha ayrıntılı ele alacağız.
En sonda özet satırı görüntülenir: Genel test sonucumuz FAILED olarak raporlanmıştır. Bir testimiz geçmiş, bir testimiz ise başarısız olmuştur.
Farklı senaryolarda test sonuçlarının nasıl göründüğünü incelediğimize göre, şimdi testlerde panic! dışında işimize yarayacak diğer makrolara bakalım.
assert! ile Sonuçları Kontrol Etme
Standart kütüphane tarafından sağlanan assert! makrosu, bir testteki belirli bir koşulun true olarak değerlendirildiğinden emin olmak istediğinizde kullanışlıdır. assert! makrosuna mantıksal (bool) bir değer üreten bir argüman veririz. Değer true ise hiçbir şey olmaz ve test geçer. Değer false ise assert! makrosu panic! çağırarak testin başarısız olmasına yol açar. assert! makrosunu kullanmak, kodumuzun amaçladığımız şekilde çalışıp çalışmadığını denetlememize yardımcı olur.
Bölüm 5, Liste 5-15’te bir Dikdortgen struct’ı ve başka bir dikdörtgeni içine alıp alamayacağını kontrol eden bir metot (kapsayabilir_mi) kullanmıştık. Bu kodu src/lib.rs dosyasına koyalım ve ardından assert! makrosunu kullanarak onun için birkaç test yazalım.
#[derive(Debug)]
struct Dikdortgen {
genislik: u32,
yukseklik: u32,
}
impl Dikdortgen {
fn kapsayabilir_mi(&self, diger: &Dikdortgen) -> bool {
self.genislik > diger.genislik && self.yukseklik > diger.yukseklik
}
}
Dikdortgen struct’ı ve kapsayabilir_mi metodukapsayabilir_mi metodu bir bool döndürür; bu da onu assert! makrosu için biçilmiş kaftan yapar. Liste 11-6’da genişliği 8, yüksekliği 7 olan bir Dikdortgen örneği oluşturup bunun genişliği 5, yüksekliği 1 olan başka bir Dikdortgen örneğini tutabildiğini doğrulayarak kapsayabilir_mi metodunu sınayan bir test yazıyoruz.
#[derive(Debug)]
struct Dikdortgen {
genislik: u32,
yukseklik: u32,
}
impl Dikdortgen {
fn kapsayabilir_mi(&self, diger: &Dikdortgen) -> bool {
self.genislik > diger.genislik && self.yukseklik > diger.yukseklik
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn buyuk_kucugu_kapsayabilir_mi() {
let buyuk = Dikdortgen {
genislik: 8,
yukseklik: 7,
};
let kucuk = Dikdortgen {
genislik: 5,
yukseklik: 1,
};
assert!(buyuk.kapsayabilir_mi(&kucuk));
}
}
kapsayabilir_mi testitests modülünün içindeki use super::*; satırına dikkat edin. tests modülü, Bölüm 7’deki “Modül Ağacındaki Bir Öğeye Başvurmak İçin Yollar” başlığında ele aldığımız olağan görünürlük kurallarına uyan sıradan bir modüldür. tests modülü bir iç modül olduğundan, dış modülde yer alan ve test edilen kodu iç modülün kapsamına dahil etmemiz gerekir. Burada bir glob (*) operatörü kullandığımız için dış modülde tanımladığımız her şey bu tests modülü içinde erişilebilir hale gelir.
Testimizi buyuk_kucugu_kapsayabilir_mi olarak adlandırdık ve ihtiyacımız olan iki Dikdortgen örneğini oluşturduk. Ardından assert! makrosunu çağırıp ona buyuk.kapsayabilir_mi(&kucuk) ifadesinin sonucunu geçirdik. Bu ifadenin true döndürmesi gerekir, dolayısıyla testimiz geçmelidir. Haydi deneyip görelim!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 1 test
test tests::buyuk_kucugu_kapsayabilir_mi ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Test geçti! Şimdi bir test daha ekleyelim ve bu kez küçük bir dikdörtgenin kendisinden büyük bir dikdörtgeni tutamayacağını doğrulayalım:
Dosya adı: src/lib.rs
#[derive(Debug)]
struct Dikdortgen {
genislik: u32,
yukseklik: u32,
}
impl Dikdortgen {
fn kapsayabilir_mi(&self, diger: &Dikdortgen) -> bool {
self.genislik > diger.genislik && self.yukseklik > diger.yukseklik
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn buyuk_kucugu_kapsayabilir_mi() {
// --snip--
let buyuk = Dikdortgen {
genislik: 8,
yukseklik: 7,
};
let kucuk = Dikdortgen {
genislik: 5,
yukseklik: 1,
};
assert!(buyuk.kapsayabilir_mi(&kucuk));
}
#[test]
fn kucuk_buyugu_tutamaz() {
let buyuk = Dikdortgen {
genislik: 8,
yukseklik: 7,
};
let kucuk = Dikdortgen {
genislik: 5,
yukseklik: 1,
};
assert!(!kucuk.kapsayabilir_mi(&buyuk));
}
}
Bu senaryoda kapsayabilir_mi metodunun doğru sonucu false olduğundan, sonucu assert! makrosuna geçirmeden önce ! operatörüyle değillememiz gerekir. Böylece kapsayabilir_mi metodu false döndürdüğünde testimiz geçecektir:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::buyuk_kucugu_kapsayabilir_mi ... ok
test tests::kucuk_buyugu_tutamaz ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
İki test de geçti! Şimdi kodumuza bir hata yerleştirdiğimizde test sonuçlarımıza ne olacağını görelim. Genişlikleri karşılaştırırken büyüktür işaretini (>) küçüktür işaretiyle (<) değiştirerek kapsayabilir_mi metodunun uygulamasını bozalım:
#[derive(Debug)]
struct Dikdortgen {
genislik: u32,
yukseklik: u32,
}
// --snip--
impl Dikdortgen {
fn kapsayabilir_mi(&self, diger: &Dikdortgen) -> bool {
self.genislik < diger.genislik && self.yukseklik > diger.yukseklik
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn buyuk_kucugu_kapsayabilir_mi() {
let buyuk = Dikdortgen {
genislik: 8,
yukseklik: 7,
};
let kucuk = Dikdortgen {
genislik: 5,
yukseklik: 1,
};
assert!(buyuk.kapsayabilir_mi(&kucuk));
}
#[test]
fn kucuk_buyugu_tutamaz() {
let buyuk = Dikdortgen {
genislik: 8,
yukseklik: 7,
};
let kucuk = Dikdortgen {
genislik: 5,
yukseklik: 1,
};
assert!(!kucuk.kapsayabilir_mi(&buyuk));
}
}
Testleri şimdi çalıştırmak şu çıktıyı üretir:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::buyuk_kucugu_kapsayabilir_mi ... FAILED
test tests::kucuk_buyugu_tutamaz ... ok
failures:
---- tests::buyuk_kucugu_kapsayabilir_mi stdout ----
thread 'tests::buyuk_kucugu_kapsayabilir_mi' (6020788) panicked at src/lib.rs:28:9:
assertion failed: buyuk.kapsayabilir_mi(&kucuk)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::buyuk_kucugu_kapsayabilir_mi
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Testlerimiz hatayı yakaladı! buyuk.genislik değeri 8 ve kucuk.genislik değeri 5 olduğundan, kapsayabilir_mi içindeki genişlik karşılaştırması artık false döndürür; çünkü 8, 5’ten küçük değildir.
assert_eq! ve assert_ne! ile Eşitliği Test Etme
İşlevselliği doğrulamanın yaygın bir yolu, test edilen kodun ürettiği sonuç ile döndürmesini beklediğiniz değer arasında eşitlik kontrolü yapmaktır. Bunu assert! makrosuna == operatörünü kullanan bir ifade geçirerek de yapabilirsiniz. Ancak bu o kadar sık yapılan bir denetimdir ki standart kütüphane bu işi daha pratik biçimde gerçekleştirmek için bir çift makro sunar: assert_eq! ve assert_ne!. Bu makrolar iki argümanı sırasıyla eşitlik veya eşitsizlik açısından karşılaştırır. Üstelik doğrulama başarısız olduğunda her iki değeri de ekrana yazdırarak testin neden başarısız olduğunu görmeyi kolaylaştırırlar; oysa assert! makrosu false sonucuna yol açan değerleri göstermeden yalnızca == ifadesinin false döndürdüğünü bildirir.
Liste 11-7’de parametresine 2 ekleyen iki_ekle adında bir fonksiyon yazıyor ve ardından bu fonksiyonu assert_eq! makrosuyla test ediyoruz.
pub fn iki_ekle(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iki_ekliyor() {
let sonuc = iki_ekle(2);
assert_eq!(sonuc, 4);
}
}
assert_eq! makrosunu kullanarak iki_ekle fonksiyonunu test etmeTestin geçip geçmediğini kontrol edelim!
$ cargo test
Compiling toplayici v0.1.0 (file:///projects/toplayici)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/toplayici-92948b65e88960b4)
running 1 test
test tests::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
iki_ekle(2) çağrısının sonucunu tutan sonuc adında bir değişken oluşturuyoruz. Ardından assert_eq! makrosuna argüman olarak sonuc ve 4 değerlerini geçiriyoruz. Bu testin çıktı satırı test tests::iki_ekliyor ... ok şeklindedir ve ok metni testimizin geçtiğini gösterir!
assert_eq! makrosunun başarısız olduğunda nasıl bir çıktı ürettiğini görmek için kodumuza bir hata ekleyelim. iki_ekle fonksiyonunun uygulamasını 2 yerine 3 ekleyecek şekilde değiştirin:
pub fn iki_ekle(a: u64) -> u64 {
a + 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iki_ekliyor() {
let sonuc = iki_ekle(2);
assert_eq!(sonuc, 4);
}
}
Testleri tekrar çalıştırın:
$ cargo test
Compiling toplayici v0.1.0 (file:///projects/toplayici)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/toplayici-92948b65e88960b4)
running 1 test
test tests::iki_ekliyor ... FAILED
failures:
---- tests::iki_ekliyor stdout ----
thread 'tests::iki_ekliyor' (6020955) panicked at src/lib.rs:12:9:
assertion `left == right` failed
left: 5
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::iki_ekliyor
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Testimiz hatayı yakaladı! tests::iki_ekliyor testi başarısız oldu; hata mesajı bize başarısız olan doğrulamanın left == right olduğunu ve left (sol) ile right (sağ) değerlerinin ne olduğunu açıkça söylüyor. Bu bilgi hata ayıklamaya hemen başlamamıza yardımcı olur: iki_ekle(2) çağrısının sonucunu verdiğimiz left argümanı 5 iken right argümanı 4 idi. Çok sayıda testimizin bulunduğu projelerde bu çıktının ne kadar işe yarayacağını tahmin edebilirsiniz.
Bazı programlama dillerinde ve test çerçevelerinde eşitlik doğrulama fonksiyonlarının parametrelerine expected (beklenen) ve actual (gerçekleşen) adı verilir; dolayısıyla argümanları belirttiğimiz sıra önem taşır. Rust’ta ise bu parametreler left ve right olarak adlandırılır; beklediğimiz değer ile kodun ürettiği değeri hangi sırayla yazdığımızın bir önemi yoktur. Bu testteki doğrulamayı assert_eq!(4, sonuc) olarak da yazabilirdik ve başarısızlık durumunda yine assertion `left == right` failed şeklinde aynı hata mesajını alırdık.
assert_ne! makrosu ise verdiğimiz iki değer birbirine eşit değilse geçer, eşitse başarısız olur. Bu makro en çok bir değerin ne olacağından emin olmadığımız, ancak kesinlikle ne olmaması gerektiğini bildiğimiz durumlarda kullanışlıdır. Örneğin girdisini bir şekilde değiştirmesi garanti edilen, ancak bu değişikliğin biçimi testleri çalıştırdığımız haftanın gününe bağlı olan bir fonksiyonu test ediyorsak, doğrulanabilecek en iyi koşul fonksiyon çıktısının girdiye eşit olmaması olabilir.
Perde arkasında assert_eq! ve assert_ne! makroları sırasıyla == ve != operatörlerini kullanır. Doğrulamalar başarısız olduğunda bu makrolar argümanlarını hata ayıklama (debug) biçimlendirmesiyle yazdırır; bu da karşılaştırılan değerlerin hem PartialEq hem de Debug trait’lerini uygulaması gerektiği anlamına gelir. Tüm ilkel türler ve standart kütüphane türlerinin büyük çoğunluğu bu trait’leri halihazırda uygular. Kendi tanımladığınız struct ve enum’larda ise türlerin eşitliğini sınayabilmek için PartialEq trait’ini, doğrulama başarısız olduğunda değerleri yazdırabilmek için de Debug trait’ini uygulamanız gerekir. Bölüm 5’teki Liste 5-12’de değindiğimiz üzere her iki trait de türetilebilir (derivable) olduğundan, bu işlem genellikle struct veya enum tanımınıza #[derive(PartialEq, Debug)] özniteliğini eklemek kadar basittir. Bu ve diğer türetilebilir trait’ler hakkında daha fazla ayrıntı için Ek C, “Türetilebilir Trait’ler” bölümüne bakabilirsiniz.
Not: Testiniz başarılı olsa bile assert_eq!(sol, sag) ve assert_ne!(sol, sag) makroları her iki trait’in de uygulanmış olmasını derleme zamanında zorunlu tutar: Makro genişletildiğinde karşılaştırma için PartialEq (== / !=), başarısızlık kolundaki hata mesajı için ise Debug ({:?}) biçimlendirme kodu üretilir. Tam eşitlik (Eq) yerine kısmi eşitlik (PartialEq) istenmesinin nedeni, NaN != NaN kuralı yüzünden Eq uygulamayan f32/f64 gibi kayan noktalı sayıların da testlerde karşılaştırılabilmesini sağlamaktır. Kendi struct veya enum türünüze #[derive(PartialEq, Debug)] eklediğinizde, o türün içindeki tüm alanların da bu iki trait’i uygulaması gerektiğini unutmayın.
Özel Başarısızlık Mesajları Ekleme
Başarısızlık mesajıyla birlikte yazdırılacak özel bir açıklamayı assert!, assert_eq! ve assert_ne! makrolarına isteğe bağlı argümanlar olarak da ekleyebilirsiniz. Zorunlu argümanlardan sonra belirtilen tüm ek argümanlar format! makrosuna (Bölüm 8’deki “+ veya format! ile Birleştirme” başlığında ele alınmıştı) aktarılır; dolayısıyla {} yer tutucuları içeren bir biçimlendirme metni ve bu yer tutuculara yerleşecek değerleri geçirebilirsiniz. Özel mesajlar bir doğrulamanın ne anlama geldiğini belgelemek açısından oldukça yararlıdır; test başarısız olduğunda koddaki sorunun ne olduğuna dair çok daha net bir fikir verir.
Örneğin insanları ismen selamlayan bir fonksiyonumuz olduğunu ve fonksiyona geçirdiğimiz ismin çıktıda yer alıp almadığını test etmek istediğimizi varsayalım:
Dosya adı: src/lib.rs
pub fn selamlama(isim: &str) -> String {
format!("Merhaba {isim}!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selamlama_isim_iceriyor() {
let sonuc = selamlama("Carol");
assert!(sonuc.contains("Carol"));
}
}
Bu programa ilişkin gereksinimler henüz kesinleşmedi ve selamlamanın başındaki Merhaba metninin ileride değişeceğinden neredeyse eminiz. Gereksinimler her değiştiğinde testi de güncellemek zorunda kalmak istemediğimiz için, selamlama fonksiyonundan dönen değer üzerinde tam eşitlik kontrolü yapmak yerine yalnızca çıktının girdi parametresindeki metni içerdiğini doğrulamayı tercih ettik.
Şimdi selamlama fonksiyonunu isim parametresini dışarıda bırakacak şekilde değiştirerek bu koda bir hata ekleyelim ve varsayılan test başarısızlığının nasıl göründüğüne bakalım:
pub fn selamlama(isim: &str) -> String {
String::from("Merhaba!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selamlama_isim_iceriyor() {
let sonuc = selamlama("Carol");
assert!(sonuc.contains("Carol"));
}
}
Bu testi çalıştırmak aşağıdaki çıktıyı üretir:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::selamlama_isim_iceriyor ... FAILED
failures:
---- tests::selamlama_isim_iceriyor stdout ----
thread 'tests::selamlama_isim_iceriyor' (6021143) panicked at src/lib.rs:12:9:
assertion failed: sonuc.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::selamlama_isim_iceriyor
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Bu sonuç yalnızca doğrulamanın başarısız olduğunu ve hangi satırda gerçekleştiğini bildirir. Daha kullanışlı bir başarısızlık mesajı, selamlama fonksiyonundan dönen gerçek değeri de yazdırırdı. Şimdi özel bir başarısızlık mesajı ekleyelim. Bu mesaj, selamlama fonksiyonundan aldığımız gerçek değerle doldurulan bir yer tutucu içeren bir biçimlendirme dizgisinden oluşsun:
pub fn selamlama(isim: &str) -> String {
String::from("Merhaba!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selamlama_isim_iceriyor() {
let sonuc = selamlama("Carol");
assert!(
sonuc.contains("Carol"),
"Selamlama isim içermiyordu, değer `{sonuc}` idi"
);
}
}
Artık testi çalıştırdığımızda çok daha bilgilendirici bir hata mesajı alırız:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::selamlama_isim_iceriyor ... FAILED
failures:
---- tests::selamlama_isim_iceriyor stdout ----
thread 'tests::selamlama_isim_iceriyor' (6021333) panicked at src/lib.rs:12:9:
Selamlama isim içermiyordu, değer `Merhaba!` idi
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::selamlama_isim_iceriyor
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Test çıktısında gerçekten elde ettiğimiz değeri görebiliyoruz; bu da beklediğimiz sonuç yerine gerçekte ne olup bittiğini anlayıp hatayı ayıklamamıza yardımcı olur.
should_panic ile Panikleri Kontrol Etme
Dönüş değerlerini kontrol etmenin yanı sıra, kodumuzun hata durumlarını beklediğimiz gibi ele alıp almadığını denetlemek de önemlidir. Örneğin Bölüm 9, Liste 9-13’te oluşturduğumuz Tahmin türünü düşünün. Tahmin türünü kullanan diğer kodlar, Tahmin örneklerinin yalnızca 1 ile 100 arasındaki değerleri barındıracağı güvencesine dayanır. Bu aralığın dışındaki bir değerle Tahmin örneği oluşturma girişiminin panikle sonuçlandığından emin olan bir test yazabiliriz.
Bunu test fonksiyonumuza should_panic özniteliğini ekleyerek yaparız. Fonksiyonun içindeki kod paniklerse test geçer; kod paniklemezse test başarısız olur.
Liste 11-8, Tahmin::new fonksiyonunun hata koşullarının beklediğimiz durumda gerçekleştiğini denetleyen bir testi gösterir.
pub struct Tahmin {
deger: i32,
}
impl Tahmin {
pub fn new(deger: i32) -> Tahmin {
if deger < 1 || deger > 100 {
panic!(
"Tahmin değeri 1 ile 100 arasında olmalıdır, {deger} alındı."
);
}
Tahmin { deger }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn yuzden_buyuk() {
Tahmin::new(200);
}
}
panic! çağrısına neden olacağını test etme#[should_panic] özniteliğini #[test] özniteliğinden sonra ve uygulanacağı test fonksiyonundan hemen önce yerleştiririz. Bu test geçtiğinde elde edilen sonuca bakalım:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::yuzden_buyuk - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Harika görünüyor! Şimdi new fonksiyonundaki değerin 100’den büyük olması durumunda panikleme koşulunu kaldırarak kodumuza bir hata ekleyelim:
pub struct Tahmin {
deger: i32,
}
// --snip--
impl Tahmin {
pub fn new(deger: i32) -> Tahmin {
if deger < 1 {
panic!(
"Tahmin değeri 1 ile 100 arasında olmalıdır, {deger} alındı."
);
}
Tahmin { deger }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn yuzden_buyuk() {
Tahmin::new(200);
}
}
Liste 11-8’deki testi çalıştırdığımızda test başarısız olacaktır:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::yuzden_buyuk - should panic ... FAILED
failures:
---- tests::yuzden_buyuk stdout ----
note: test did not panic as expected at src/lib.rs:23:8
failures:
tests::yuzden_buyuk
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Bu durumda çok açıklayıcı bir mesaj almayız; ancak test fonksiyonuna baktığımızda #[should_panic] ile işaretlendiğini görürüz. Aldığımız başarısızlık sonucu, test fonksiyonundaki kodun paniğe yol açmadığı anlamına gelir.
Yalnızca should_panic kullanan testler yeterince kesin olmayabilir. Bir should_panic testi, beklediğimizden tamamen farklı bir nedenle paniklese bile geçer. should_panic testlerini daha kesin hale getirmek için should_panic özniteliğine isteğe bağlı expected parametresini ekleyebiliriz. Test altyapısı, panik mesajının belirtilen metni içerip içermediğini kontrol eder. Örneğin Liste 11-9’da new fonksiyonunun, değerin çok küçük ya da çok büyük olmasına bağlı olarak farklı mesajlarla paniklediği güncellenmiş Tahmin koduna bakalım.
pub struct Tahmin {
deger: i32,
}
// --snip--
impl Tahmin {
pub fn new(deger: i32) -> Tahmin {
if deger < 1 {
panic!(
"Tahmin değeri 1'den büyük veya 1'e eşit olmalıdır, {deger} alındı."
);
} else if deger > 100 {
panic!(
"Tahmin değeri 100'den küçük veya 100'e eşit olmalıdır, {deger} alındı."
);
}
Tahmin { deger }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "100'den küçük veya 100'e eşit")]
fn yuzden_buyuk() {
Tahmin::new(200);
}
}
panic! oluşumunu test etmeBu test geçecektir; çünkü should_panic özniteliğinin expected parametresine yazdığımız metin, Tahmin::new fonksiyonunun paniklerken ürettiği mesajın bir alt dizgisidir. Beklediğimiz panik mesajının tamamını, yani Tahmin değeri 100'den küçük veya 100'e eşit olmalıdır, 200 alındı. metnini de belirtebilirdik. Neyi belirteceğiniz, panik mesajının ne kadarının benzersiz veya dinamik olduğuna ve testinizin ne ölçüde kesin olmasını istediğinize bağlıdır. Bu örnekte panik mesajının bir alt dizgisini vermek, test fonksiyonundaki kodun else if deger > 100 kolunu çalıştırdığından emin olmak için yeterlidir.
expected mesajına sahip bir should_panic testi başarısız olduğunda ne olacağını görmek için, if deger < 1 ve else if deger > 100 bloklarının gövdelerini birbiriyle değiştirerek kodumuza bir kez daha hata ekleyelim:
pub struct Tahmin {
deger: i32,
}
impl Tahmin {
pub fn new(deger: i32) -> Tahmin {
if deger < 1 {
panic!(
"Tahmin değeri 100'den küçük veya 100'e eşit olmalıdır, {deger} alındı."
);
} else if deger > 100 {
panic!(
"Tahmin değeri 1'den büyük veya 1'e eşit olmalıdır, {deger} alındı."
);
}
Tahmin { deger }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "100'den küçük veya 100'e eşit")]
fn yuzden_buyuk() {
Tahmin::new(200);
}
}
Bu kez should_panic testini çalıştırdığımızda test başarısız olur:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::yuzden_buyuk - should panic ... FAILED
failures:
---- tests::yuzden_buyuk stdout ----
thread 'tests::yuzden_buyuk' (6021675) panicked at src/lib.rs:12:13:
Tahmin değeri 1'den büyük veya 1'e eşit olmalıdır, 200 alındı.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: "Tahmin değeri 1'den büyük veya 1'e eşit olmalıdır, 200 alındı."
expected substring: "100'den küçük veya 100'e eşit"
failures:
tests::yuzden_buyuk
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Hata mesajı, testin beklediğimiz gibi gerçekten paniklediğini ancak panik mesajının beklenen 100'den küçük veya 100'e eşit dizgisini içermediğini gösterir. Bu durumda elde ettiğimiz gerçek panik mesajı Tahmin değeri 1'den büyük veya 1'e eşit olmalıdır, 200 alındı. şeklindeydi. Böylece hatamızın nerede olduğunu hemen bulabiliriz!
Testlerde Result<T, E> Kullanma
Şimdiye kadarki tüm testlerimiz başarısız olduklarında panikliyordu. Testlerimizi Result<T, E> döndürecek şekilde de yazabiliriz! İşte Liste 11-1’deki testin paniklemek yerine Result<T, E> kullanacak ve hata durumunda bir Err döndürecek biçimde yeniden yazılmış hali:
pub fn topla(sol: u64, sag: u64) -> u64 {
sol + sag
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calisiyor() -> Result<(), String> {
let sonuc = topla(2, 2);
if sonuc == 4 {
Ok(())
} else {
Err(String::from("iki artı iki dört etmez"))
}
}
}
calisiyor fonksiyonu artık Result<(), String> dönüş türüne sahiptir. Fonksiyon gövdesinde assert_eq! makrosunu çağırmak yerine, test başarılı olduğunda Ok(()), başarısız olduğunda ise içinde sorunu açıklayan bir String barındıran Err varyantı döndürüyoruz.
Testleri Result<T, E> döndürecek şekilde yazmak, test gövdesinde soru işareti operatörünü (?) kullanmanıza olanak tanır; bu da içindeki herhangi bir adım Err varyantı döndürdüğünde başarısız olması gereken çok adımlı testleri yazmak için son derece pratik bir yoldur.
Result<T, E> kullanan testlerde #[should_panic] özniteliğini kullanamazsınız. Bir işlemin Err varyantı döndürdüğünü doğrulamak için Result<T, E> değeri üzerinde soru işareti operatörünü (?) kullanmayın (aksi hâlde ? operatörü hatayı dışarı yayarak testi başarısız kılar). Bunun yerine assert!(deger.is_err()) ifadesini kullanın.
Not: Bir test birden fazla hata türü üretebilen adımlar içeriyorsa (örneğin önce bir dosya okuyup sonra içindeki metni sayıya çeviriyorsa) her adımın hatasını String’e dönüştürmek zahmetlidir. Bu gibi durumlarda testin dönüş türünü, Bölüm 9’da gördüğünüz Box<dyn Error> ile Result<(), Box<dyn std::error::Error>> olarak yazmak yaygın bir alışkanlıktır: ? operatörü Error trait’ini uygulayan her hatayı kendiliğinden bu kutuya dönüştürür, test ise ilk Err döndüren adımda hata mesajıyla birlikte başarısız olur. Test başarısız olduğunda test çalıştırıcısı, Err içindeki değeri Debug biçimlendirmesiyle ({:?}) ekrana yazdırır; dolayısıyla E türünün Debug trait’ini uygulaması gerekir.
Artık test yazmanın çeşitli yollarını öğrendiğinize göre, testlerimizi çalıştırdığımızda neler olup bittiğine bakalım ve cargo test ile kullanabileceğimiz farklı seçenekleri inceleyelim.