Test Güdümlü Geliştirme (Test-Driven Development) ile İşlevsellik Eklemek
Artık src/lib.rs içerisindeki arama mantığını main fonksiyonundan ayırdığımıza göre, kodumuzun çekirdek işlevselliği için test yazmak çok daha kolaydır. İkili dosyamızı komut satırından çağı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ımları içeren 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 ç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.
-
- 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ı olabilir. Testin geçmesini sağlayan kodu yazmadan önce testi yazmak, süreç boyunca yüksek test kapsamını sürdürmeye yardımcı olur.
Dosya içeriklerinde aranan string’i gerçekten arayacak ve sorguyla eşleşen (match) satırların bir listesini üretecek işlevselliğin uygulamasını test güdümlü olarak yapacağız. Bu işlevselliği ara adında bir fonksiyona ekleyeceğiz.
Başarısız Olan Bir Test Yazmak
Bölüm 11’de yaptığımız gibi src/lib.rs dosyasına test fonksiyonu içeren bir tests modülü ekleyeceğiz. Test fonksiyonu, ara fonksiyonunun sahip olmasını istediğimiz davranışı belirtir: Bir sorgu ve aranacak metni alacak ve metinden yalnızca sorguyu barındıran 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 fonksiyonu için sahip olmak istediğimiz işlevselliğe dair başarısız bir test oluşturmakBu test "güven" string’ini (dizgisini) arar. Aradığımız metin üç satırdan oluşuyor ve bunlardan sadece biri "güven" içeriyor (açılış çift tırnağından sonraki ters eğik çizginin Rust’a bu string sabitinin içeriklerinin başına yeni satır karakteri koymamasını söylediğine dikkat edin). ara fonksiyonundan dönen değerin yalnızca beklediğimiz satırı barındırdığını doğruluyoruz.
Bu testi çalıştırırsak, unimplemented! (uygulanmadı) makrosu “not implemented” (uygulanmadı) mesajıyla paniklediği için şu an başarısız olacaktır. TDD ilkelerine uygun olarak, 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 testin paniklememesini sağlayacak kadar kod ekleme yönünde küçük bir adım atacağız. Böylece test derlenmeli (compile) ve başarısız olmalıdır, çünkü boş bir vektör "güvenli, hızlı, üretken." satırını barındıran bir vektörle eşleşmez.
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 tanımlamakŞimdi ara fonksiyonunun imzasında açık bir 'a ömrü (lifetime) tanımlamaya ve bu ömrü icerik argümanı ve dönüş değeriyle birlikte kullanmaya neden ihtiyacımız olduğunu tartışalım. Bölüm 10’dan hatırlayın ki, ömür parametreleri hangi argümanın ömrünün dönüş değerinin ömrüne bağlandığını belirtir. Bu durumda, döndürülen vektörün (sorgu argümanı yerine) icerik argümanının dilimlerine referans veren string dilimleri içermesi gerektiğini belirtiyoruz.
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ı (live as long as) söylüyoruz. Bu önemlidir! Bir dilim (slice) tarafından atıfta bulunulan (referenced) verinin, referansın geçerli olması için geçerli olması (valid) gerekir; eğer derleyici icerik yerine sorgu’nun string dilimlerini oluşturduğumuzu varsayarsa, güvenlik denetimini yanlış yapacaktır.
Ömür açıklamalarını unutup bu fonksiyonu derlemeye çalışırsak şu hatayı alırız:
$ cargo olustur
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:51
|
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 bilemez, bu nedenle bunu açıkça söylememiz gerekir. Yardım metninin tüm parametreler ve çıktı türü için aynı ömür parametresinin belirtilmesini önerdiğine dikkat edin, ki bu yanlıştır! Tüm metnimizi barındıran parametre icerik olduğundan ve metnin eşleşen parçalarını döndürmek istediğimizden, ömür sözdizimini kullanarak dönüş değerine bağlanması gereken tek parametrenin icerik olduğunu biliyoruz.
Diğer programlama dilleri, imzadaki argümanları dönüş değerlerine bağlamanızı gerektirmez ancak bu pratik zamanla kolaylaşacaktır. Bu örneği Bölüm 10’daki “Ömürlerle Referansları Doğrulamak” bölümündeki örneklerle karşılaştırmak isteyebilirsiniz.
Testi Geçecek Kodu Yazmak
Şu anda testimiz başarısız oluyor çünkü her zaman boş bir vektör döndürüyoruz. Bunu düzeltmek ve ara’yı uygulamak için programımızın şu adımları izlemesi gerekir:
- İçeriğin her satırı üzerinde yineleme yapın.
- Satırın sorgu string’imizi içerip içermediğini kontrol edin.
- İçeriyorsa, döndürdüğümüz değerler listesine ekleyin.
- İçermiyorsa, hiçbir şey yapmayın.
- Eşleşen sonuçların listesini döndürün.
Satırlar üzerinde yineleme yapmakla başlayarak her bir adım üzerinde çalışalım.
lines Metodu ile Satırlar Üzerinde Yineleme (Iterating) Yapmak
Rust, stringlerin satır satır yinelemesini işlemek için Liste 12-17’de gösterildiği gibi çalışan ve uygun bir şekilde lines (satırlar) olarak adlandırılan yararlı bir metoda sahiptir. Bunun 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’teki her bir satır üzerinde yineleme yapmaklines metodu bir yineleyici döndürür. Yineleyicileri Bölüm 13’te derinlemesine tartışacağız. Ancak yineleyicinin bu kullanım şeklini Liste 3-5’te gördüğünüzü hatırlayın; burada bir koleksiyondaki her bir öğe üzerinde bir miktar kod çalıştırmak için bir yineleyici ile birlikte bir for döngüsü kullanmıştık.
Her Satırda Sorguyu (Query) Aramak
Sırada o anki satırın sorgu string’imizi barındırıp barındırmadığını kontrol edeceğiz. Neyse ki stringlerin bunu bizim için yapan contains (içerir) adlı yararlı bir metodu var! Liste 12-18’de gösterildiği gibi ara fonksiyonuna contains metodu çağrısı ekleyin. Bunun hala 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’daki string’i barındırıp barındırmadığını görmek için işlevsellik eklemekŞu anda işlevselliği oluşturuyoruz. Kodun derlenmesini sağlamak için fonksiyon imzasında yapacağımızı belirttiğimiz gibi gövdeden bir değer döndürmemiz (return) gerekiyor.
Eşleşen Satırları Depolamak
Bu fonksiyonu bitirmek 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 satir’ı vektörde depolamak için push (it) metodunu çağırabiliriz. for döngüsünden sonra, Liste 12-19’da gösterildiği gibi 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 sadece sorgu içeren satırları döndürmelidir 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.00s
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 çalıştığını biliyoruz!
Bu noktada, aynı işlevselliği sürdürmek adına testleri geçer durumda tutarken arama fonksiyonunun uygulamasını yeniden düzenleme fırsatlarını değerlendirebiliriz. Arama fonksiyonundaki kod çok kötü değil, ancak yineleyicilerin bazı yararlı özelliklerinden yararlanmıyor. Bölüm 13’te yineleyicileri ayrıntılı olarak inceleyeceğimiz bu örneğe geri döneceğiz ve nasıl iyileştireceğimize bakacağız.
Artık programın tamamı çalışmalı! Önce şiirden tam olarak bir satır 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 kelime 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 olmayan bir kelimeyi (örneğin monomorphization) aradığımızda hiçbir satır almadığımızdan 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 versiyonunu oluşturduk ve uygulamaların nasıl yapılandırılacağı hakkında çok şey öğrendik. Ayrıca dosya giriş ve çıkışları, ömürler, testler ve komut satırı ayrıştırma hakkında biraz bilgi edindik.
Bu projeyi tamamlamak için çevre değişkenleriyle (environment variables) 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 yararlıdır.