Test Güdümlü Geliştirme ile İşlevsellik Ekleme
Arama mantığını src/lib.rs içine alıp main fonksiyonundan ayırdığımıza göre, artık kodumuzun çekirdek işlevselliği için test yazmak çok daha kolay. İkili dosyamızı komut satırından çalıştırmak zorunda kalmadan fonksiyonları çeşitli argümanlarla doğrudan çağırabilir ve dönüş değerlerini kontrol edebiliriz.
Bu bölümde, aşağıdaki adımlardan oluşan test güdümlü geliştirme (test-driven development — TDD) sürecini kullanarak minigrep programına arama mantığını ekleyeceğiz:
- Başarısız olan bir test yazın ve beklediğiniz nedenden dolayı başarısız olduğundan emin olmak için testi çalıştırın.
- Yeni testin geçmesini sağlayacak kadar kod yazın veya mevcut kodu değiştirin.
- Yeni eklediğiniz veya değiştirdiğiniz kodu yeniden düzenleyin ve testlerin geçmeye devam ettiğinden emin olun.
- Birinci adımdan itibaren tekrarlayın!
Yazılım geliştirmenin pek çok yolundan yalnızca biri olsa da TDD, kod tasarımını yönlendirmeye yardımcı olur. Testi geçirecek kodu yazmadan önce testin kendisini yazmak, geliştirme süreci boyunca yüksek test kapsamını korumayı sağlar.
Dosya içeriğinde sorgu metnini fiilen arayacak ve sorguyla eşleşen satırların bir listesini üretecek işlevselliği test güdümlü olarak uygulayacağız. Bu işlevselliği ara adında bir fonksiyona ekleyeceğiz.
Başarısız Olan Bir Test Yazma
Bölüm 11’de yaptığımız gibi src/lib.rs dosyasına bir test fonksiyonu barındıran tests modülü ekleyeceğiz. Test fonksiyonu, ara fonksiyonunun sahip olmasını istediğimiz davranışı tanımlar: Bir sorgu ve aranacak metni alacak, metinden yalnızca sorguyu içeren satırları döndürecektir. Liste 12-15 bu testi göstermektedir.
pub fn ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// --snip--
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tek_sonuc() {
let sorgu = "güven";
let icerik = "\
Güven:
güvenli, hızlı, üretken.
Üçünü de seç.";
assert_eq!(vec!["güvenli, hızlı, üretken."], ara(sorgu, icerik));
}
}
ara işlevselliği için başarısız bir test oluşturmaBu test "güven" string’ini arar. Üzerinde arama yaptığımız metin üç satırdan oluşur ve büyük/küçük harf duyarlılığı nedeniyle bunlardan yalnızca ikincisi küçük harfli "güven" ifadesini barındırır (açılış çift tırnağından hemen sonra gelen ters eğik çizginin \, Rust’a bu çok satırlı string sabitinin başına yeni satır karakteri koymamasını söylediğine dikkat edin). Ardından ara fonksiyonundan dönen değerin yalnızca beklediğimiz satırı içerdiğini doğruluyoruz.
Bu test derlenir; ancak çalıştırıldığında unimplemented! makrosu "not implemented" (uygulanmadı) mesajıyla paniklediği için başarısız olur. TDD ilkelerine uygun olarak küçük bir adım atalım: Liste 12-16’da gösterildiği gibi ara fonksiyonunu her zaman boş bir vektör döndürecek şekilde tanımlayarak fonksiyon çağrıldığında paniklememesini sağlayacak kadar kod ekleyelim. Böylece test, beklediğimiz asıl nedenden dolayı —boş bir vektör, "güvenli, hızlı, üretken." satırını barındıran vektörle eşleşmediği için— başarısız olacaktır.
pub fn ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tek_sonuc() {
let sorgu = "güven";
let icerik = "\
Güven:
güvenli, hızlı, üretken.
Üçünü de seç.";
assert_eq!(vec!["güvenli, hızlı, üretken."], ara(sorgu, icerik));
}
}
ara fonksiyonunu çağrıldığında paniklemeyecek kadar tanımlamaŞimdi ara fonksiyonunun imzasında neden açık bir 'a ömrü tanımlamamız ve bu ömrü icerik argümanıyla dönüş değerinde kullanmamız gerektiğini inceleyelim. Bölüm 10’dan hatırlayacağınız üzere ömür parametreleri, hangi argümanın ömrünün dönüş değerinin ömrüne bağlı olduğunu belirtir. Bu örnekte, döndürülen vektörün (sorgu argümanına değil) icerik argümanının dilimlerine referans veren string dilimleri barındıracağını ifade ediyoruz.
Başka bir deyişle Rust’a, ara fonksiyonu tarafından döndürülen verinin ara fonksiyonuna icerik argümanıyla aktarılan veri kadar uzun yaşayacağını söylüyoruz. Bu çok önemlidir! Bir referansın geçerli kalabilmesi için dilim tarafından işaret edilen verinin de geçerli olması gerekir; derleyici icerik yerine sorgu üzerinden string dilimleri oluşturduğumuzu varsayarsa güvenlik denetimini yanlış yapacaktır.
Ömür bildirimlerini unutup bu fonksiyonu derlemeye çalışırsak şu hatayı alırız:
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:46
|
1 | pub fn ara(sorgu: &str, icerik: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `sorgu` or `icerik`
help: consider introducing a named lifetime parameter
|
1 | pub fn ara<'a>(sorgu: &'a str, icerik: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error
Rust, çıktı için iki parametreden hangisine ihtiyacımız olduğunu kendiliğinden bilemez; bu yüzden bunu açıkça belirtmemiz gerekir. Derleyicinin yardım (help) metninin tüm parametreler ve çıktı türü için aynı ömür parametresini kullanmayı önerdiğine dikkat edin; oysa bu bizim durumumuzda yanlıştır! Metnimizin tamamını barındıran parametre icerik olduğundan ve biz o metnin eşleşen bölümlerini döndürmek istediğimizden, ömür sözdizimiyle dönüş değerine bağlanması gereken tek parametrenin icerik olduğunu biliyoruz.
Diğer programlama dilleri fonksiyon imzasında argümanları dönüş değerlerine bağlamanızı gerektirmez, ancak bu pratik zamanla çok daha doğal gelecektir. Bu örneği Bölüm 10’daki “Ömürlerle Referansları Doğrulama” bölümünde yer alan örneklerle karşılaştırmak isteyebilirsiniz.
Not: Derleyicinin önerdiği pub fn ara<'a>(sorgu: &'a str, icerik: &'a str) -> Vec<&'a str> imzası teknik olarak derlense bile dönüş değerinin ömrünü
gereksiz yere sorgu parametresinin ömrüne de kısıtlar. Örneğin uzun ömürlü
bir icerik metni üzerinde, içteki kısa bir kapsamda oluşturulan geçici bir
sorgu: String aradığınızı düşünün: Arama bittikten ve sorgu kapsam dışına
çıkıp düşürüldükten sonra bile dönen satır dilimlerini kullanabilmelisiniz;
çünkü bu dilimler metin baytlarını kopyalamadan (zero-copy) doğrudan
icerik içindeki parçalara işaret eder. Döndürülen Vec dilimleri saklamak
için bellek ayırabilir; kopyalamadığımız şey metnin kendisidir. İmzamızı
ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> şeklinde yazdığımızda
sorgu, Bölüm 10’da ele aldığımız ömür elizyon kuralları (lifetime elision
rules) sayesinde 'a’dan tamamen bağımsız kendi ömrünü alır; böylece sorgu
yalnızca arama süresince ödünç alınır, sonuç vektörü ise icerik yaşadığı
sürece geçerli kalır.
Testi Geçecek Kodu Yazma
Şu anda testimiz her zaman boş bir vektör döndürdüğümüz için başarısız oluyor. Bunu düzeltmek ve ara fonksiyonunu uygulamak için programımızın şu adımları izlemesi gerekir:
- İçeriğin her satırı üzerinde yineleme yapmak.
- Satırın sorgu string’imizi barındırıp barındırmadığını kontrol etmek.
- Barındırıyorsa döndüreceğimiz değerler listesine eklemek.
- Barındırmıyorsa hiçbir şey yapmamak.
- Eşleşen sonuçların listesini döndürmek.
Satırlar üzerinde yineleme yapmaktan başlayarak her adımı sırasıyla uygulayalım.
lines Metodu ile Satırlar Üzerinde Yineleme Yapma
Rust, string’ler üzerinde satır satır gezinmeyi sağlamak için amacına uygun biçimde lines olarak adlandırılmış kullanışlı bir metoda sahiptir; bu metot Liste 12-17’de gösterildiği gibi çalışır. Kodun henüz derlenmeyeceğini unutmayın.
pub fn ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> {
for satir in icerik.lines() {
// satir ile bir şeyler yapın
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tek_sonuc() {
let sorgu = "güven";
let icerik = "\
Güven:
güvenli, hızlı, üretken.
Üçünü de seç.";
assert_eq!(vec!["güvenli, hızlı, üretken."], ara(sorgu, icerik));
}
}
icerik içindeki her bir satır üzerinde yineleme yapmalines metodu bir iteratör döndürür. İteratörleri Bölüm 13’te derinlemesine ele alacağız. Ancak iteratörü bu şekilde kullanmayı daha önce Liste 3-5’te gördüğünüzü hatırlayın: Orada bir koleksiyondaki her öğe üzerinde belirli bir kodu çalıştırmak için for döngüsünü bir iteratörle birlikte kullanmıştık.
Her Satırda Sorguyu Arama
Sırada mevcut satırın sorgu string’imizi içerip içermediğini kontrol etmek var. Neyse ki string’lerin bu işi bizim yerimize yapan contains adında kullanışlı bir metodu bulunuyor! Liste 12-18’de gösterildiği gibi ara fonksiyonuna bir contains metodu çağrısı ekleyin. Bu kodun da henüz derlenmeyeceğini unutmayın.
pub fn ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> {
for satir in icerik.lines() {
if satir.contains(sorgu) {
// satir ile bir şeyler yapın
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tek_sonuc() {
let sorgu = "güven";
let icerik = "\
Güven:
güvenli, hızlı, üretken.
Üçünü de seç.";
assert_eq!(vec!["güvenli, hızlı, üretken."], ara(sorgu, icerik));
}
}
sorgu içindeki string’i barındırıp barındırmadığını kontrol edecek işlevselliği eklemeŞu anda işlevselliği adım adım inşa ediyoruz. Kodun derlenmesini sağlamak için, fonksiyon imzasında belirttiğimiz üzere gövdeden bir değer döndürmemiz gerekiyor.
Eşleşen Satırları Saklama
Bu fonksiyonu tamamlamak için döndürmek istediğimiz eşleşen satırları saklayacak bir yola ihtiyacımız var. Bunun için for döngüsünden önce değiştirilebilir bir vektör oluşturabilir ve eşleşen her satir dilimini vektöre eklemek üzere push metodunu çağırabiliriz. for döngüsünden sonra da Liste 12-19’da gösterildiği gibi bu vektörü döndürürüz.
pub fn ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> {
let mut sonuclar = Vec::new();
for satir in icerik.lines() {
if satir.contains(sorgu) {
sonuclar.push(satir);
}
}
sonuclar
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tek_sonuc() {
let sorgu = "güven";
let icerik = "\
Güven:
güvenli, hızlı, üretken.
Üçünü de seç.";
assert_eq!(vec!["güvenli, hızlı, üretken."], ara(sorgu, icerik));
}
}
Artık ara fonksiyonu yalnızca sorgu içeren satırları döndürmeli ve testimiz geçmelidir. Testi çalıştıralım:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::tek_sonuc ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Testimiz geçti; yani kodun çalıştığını biliyoruz!
Bu noktada, aynı işlevselliği koruduğumuzdan emin olmak için testleri geçer durumda tutarken arama fonksiyonunun uygulamasını yeniden düzenleme fırsatlarını değerlendirebiliriz. Arama fonksiyonundaki kod fena değil, ancak iteratörlerin bazı faydalı özelliklerinden henüz yararlanmıyor. İteratörleri ayrıntılı olarak inceleyeceğimiz Bölüm 13’te bu örneğe geri dönecek ve kodu nasıl iyileştirebileceğimize bakacağız.
Not: Uyguladığımız bu üç aşamalı döngü yazılım dünyasında Kırmızı–Yeşil–Yeniden Düzenle (Red–Green–Refactor) olarak adlandırılır: Önce beklenen nedenle başarısız olan testi görürsünüz (Kırmızı), ardından en yalın kodla testi geçirirsiniz (Yeşil) ve son olarak çalışan testlerin güvencesi altında kodu temizlersiniz (Yeniden Düzenle). Ayrıca kullandığımız str::lines metodunun platformlar arası önemli bir kolaylık sağladığına dikkat edin: Metni hem Unix tarzı (\n) hem de Windows tarzı (\r\n) satır sonlarından böler ve satır sonu karakterlerini döndürdüğü &str dilimlerine dahil etmez; böylece siir.txt hangi işletim sisteminde kaydedilmiş olursa olsun arama ve karşılaştırma sonuçlarımız birebir aynı kalır.
Artık programın tamamı çalışmalı! İlk olarak Necip Fazıl Kısakürek şiirinden tam olarak tek bir dize döndürmesi gereken bir kelimeyle deneyelim: mezar.
$ cargo run -- mezar siir.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep mezar siir.txt`
Ne taze ölüyü mezar.
Harika! Şimdi bek gibi birden fazla satırla eşleşecek bir ifadeyi deneyelim:
$ cargo run -- bek siir.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep bek siir.txt`
Ne hasta bekler sabahı,
Seni beklediğim kadar.
Ve son olarak, şiirde hiçbir yerde geçmeyen bir kelimeyi —örneğin monomorphization— aradığımızda hiçbir satır dönmediğinden emin olalım:
$ cargo run -- monomorphization siir.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization siir.txt`
Mükemmel! Klasik bir aracın kendi mini sürümünü geliştirdik ve uygulamaların nasıl yapılandırılacağı konusunda pek çok şey öğrendik. Ayrıca dosya giriş ve çıkışı, ömürler, test yazımı ve komut satırı ayrıştırma konularında da deneyim kazandık.
Bu projeyi tamamlamak için çevre değişkenleriyle nasıl çalışılacağını ve standart hataya (standard error) nasıl yazdırılacağını kısaca göstereceğiz; her ikisi de komut satırı programları yazarken oldukça faydalıdır.