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 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:

  1. 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.
  2. Yeni testin geçmesini sağlayacak kadar kod yazın veya mevcut kodu değiştirin.
  3. Yeni eklediğiniz veya değiştirdiğiniz kodu yeniden düzenleyin ve testlerin geçmeye devam ettiğinden emin olun.
  4. 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.

Dosya adı: src/lib.rs
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));
    }
}
Liste 12-15: Sahip olmayı istediğimiz ara işlevselliği için başarısız bir test oluşturma

Bu 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.

Dosya adı: src/lib.rs
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));
    }
}
Liste 12-16: 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:

  1. İçeriğin her satırı üzerinde yineleme yapmak.
  2. Satırın sorgu string’imizi barındırıp barındırmadığını kontrol etmek.
  3. Barındırıyorsa döndüreceğimiz değerler listesine eklemek.
  4. Barındırmıyorsa hiçbir şey yapmamak.
  5. 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.

Dosya adı: src/lib.rs
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));
    }
}
Liste 12-17: icerik içindeki her bir satır üzerinde yineleme yapma

lines 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.

Dosya adı: src/lib.rs
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));
    }
}
Liste 12-18: Satırın 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.

Dosya adı: src/lib.rs
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));
    }
}
Liste 12-19: Döndürebilmek için eşleşen satırları saklama

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.