Test Odaklı Geliştirme ile Kütüphanenin İşlevselliğini Geliştirme

Artık kök mantığı src/lib.rs'ye çıkardığımıza ve argüman toplama ve hata işlemeyi src/main.rs'de bıraktığımıza göre, kodumuzun temel işlevselliği için test yazmak çok daha kolay. Fonksiyonları çeşitli argümanlarla doğrudan çağırabilir ve ikili dosyamızı komut satırından çağırmak zorunda kalmadan dönüş değerlerini kontrol edebiliriz.

Bu bölümde, aşağıdaki adımlarla test odaklı geliştirme (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 çalıştırın.
  2. Yeni testin geçmesi için yeterli kodu yazın veya 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. Adım 1'den itibaren tekrarlayın!

TDD, yazılım yazmanın birçok yolundan yalnızca biri olsa da 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ın korunmasına yardımcı olur.

Dosya içeriğindeki sorgu dizgisini gerçekten arayacak ve sorguyla eşleşen satırların bir listesini üretecek işlevselliğin uygulanmasını test edeceğiz. Bu işlevselliği search adlı bir fonksiyona ekleyeceğiz.

Başarısız Bir Test Yazma

Artık onlara ihtiyacımız olmadığından, programın davranışını kontrol etmek için kullandığımız println! ifade yapılarını src/lib.rs ve src/main.rs dosyalarından kaldıralım. Daha sonra, src/lib.rs dosyasına, Bölüm 11'de yaptığımız gibi bir test fonksiyonu içeren bir tests modülü ekleyelim. Test fonksiyonu, search fonksiyonunun sahip olmasını istediğimiz davranışı belirtir: bir sorgu ve aranacak metni alır ve metinden yalnızca sorguyu içeren satırları döndürür. Liste 12-15, henüz derlenmeyecek olan bu testi göstermektedir.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-15: Keşke yapsaydık dediğimiz search fonksiyonu için başarısız bir test oluşturma

Bu test "duct" dizgisini arar. Aradığımız metin, yalnızca biri "duct" içeren üç satırdır (Açılıştaki çift tırnak işaretinden sonraki ters eğik çizginin Rust'a bu dize değişmezinin içeriğinin başına yeni satır karakteri koymamasını söylediğine dikkat edin). Arama fonksiyonundan dönen değerin sadece beklediğimiz satırı içerdiğini iddia ediyoruz.

Henüz bu testi çalıştırıp başarısız olmasını izleyemiyoruz çünkü test derlenmiyor bile: arama fonksiyonu henüz mevcut değil! TDD ilkelerine uygun olarak, Liste 12-16'da gösterildiği gibi her zaman boş bir vektör döndüren bir arama fonksiyonu tanımı ekleyerek testin derlenmesini ve çalışmasını sağlayacak kadar kod ekleyeceğiz. Ardından test derlenmeli ve başarısız olmalıdır çünkü boş bir vektör "safe, fast, productive" satırını içeren bir vektörle eşleşmez.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-16: Testimizin derlenebilmesi için search fonksiyonunun değiştirilmesi

search'ın imzasında açık bir 'a yaşam süresi tanımlamamız ve bu yaşam süresini contents argümanı ve dönüş değeri ile kullanmamız gerektiğine dikkat edin. Bölüm 10'da yaşam süresi parametrelerinin hangi argüman yaşam süresinin geri dönüş değerinin yaşam süresine bağlı olduğunu belirttiğini hatırlayın. Bu durumda, döndürülen vektörün (argüman sorgusu yerine) argüman contents'in dilimlerine referans veren dizgi dilimleri içermesi gerektiğini belirtiriz.

Başka bir deyişle, Rust'a search fonksiyonu tarafından döndürülen verilerin, contents argümanında search fonksiyonuna aktarılan veriler kadar uzun yaşayacağını söylüyoruz. Bu çok önemlidir! Referansın geçerli olabilmesi için bir dilim tarafından referans verilen verinin geçerli olması gerekir; derleyici içerik yerine sorgunun dize dilimlerini oluşturduğumuzu varsayarsa, güvenlik kontrolünü yanlış yapacaktır.

Eğer yaşam süresi ek açıklamalarını unutur ve bu fonksiyonu derlemeye çalışırsak, bu hatayı alırız:

$ cargo build
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
  --> src/lib.rs:28:51
   |
28 | pub fn search(query: &str, contents: &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 `query` or `contents`
help: consider introducing a named lifetime parameter
   |
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
   |              ++++         ++                 ++              ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` due to previous error

Rust bu iki argümandan hangisine ihtiyacımız olduğunu bilemez, bu yüzden bunu ona açıkça söylememiz gerekir. contents tüm metnimizi içeren argüman olduğundan ve bu metnin eşleşen kısımlarını döndürmek istediğimizden, contents'in lifetime söz dizimini kullanarak dönüş değerine bağlanması gereken argüman olduğunu biliyoruz.

Diğer programlama dilleri, imzada argümanları dönüş değerlerine bağlamanızı gerektirmez, ancak bu uygulama zamanla daha kolay hale gelecektir. Bu örneği Bölüm 10'daki “Referansları Yaşam Süreleri ile Doğrulama” bölümü ile karşılaştırmak isteyebilirsiniz.

Şimdi testi çalıştıralım:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 0.97s
     Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... FAILED

failures:

---- tests::one_result stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `["safe, fast, productive."]`,
 right: `[]`', src/lib.rs:44:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::one_result

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'

Harika, test tam da beklediğimiz gibi başarısız oldu. Hadi testi geçelim!

Testi Geçirmek İçin Kod Yazma

Şu anda, her zaman boş bir vektör döndürdüğümüz için testimiz başarısız oluyor. Bunu düzeltmek ve search'ü uygulamak için programımızın aşağıdaki adımları izlemesi gerekir:

  • İçeriğin her satırını yineleyin.
  • Satırın sorgu dizemizi içerip içermediğini kontrol edin.
  • Eğer içeriyorsa, döndürdüğümüz değerler listesine ekleyin.
  • Eğer içermiyorsa, hiçbir şey yapmayın.
  • Eşleşen sonuçların listesini döndürün.

Satırlar arasında yineleme ile başlayarak her adımda çalışalım.

lines Metodu ile Satırlar Arasında Yineleme

Rust, dizgilerin satır satır yinelenmesini işlemek için uygun bir şekilde satır olarak adlandırılan ve Liste 12-17'de gösterildiği gibi çalışan yararlı bir metoda sahiptir. Bunun henüz derlenmeyeceğini unutmayın.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        // do something with line
    }
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-17: contents'teki her satırı yineleme

lines metodu bir yineleyici döndürür. Yineleyiciler hakkında Bölüm 13'te derinlemesine konuşacağız, ancak bir yineleyici kullanmanın bu yolunu, bir koleksiyondaki her bir öğe üzerinde bazı kodlar çalıştırmak için bir yineleyici ile bir for döngüsü kullandığımız Liste 3-5'te gördüğünüzü hatırlayın.

Sorgu için Her Satırı Arama

Ardından, geçerli satırın sorgu dizemizi içerip içermediğini kontrol edeceğiz. Neyse ki, String bunu bizim için yapan contains adında yararlı bir metoda sahiptir! Liste 12-18'de gösterildiği gibi, search fonksiyonuna contains metoduna bir çağrı ekleyin. Bunun henüz derlenmeyeceğini unutmayın.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // do something with line
        }
    }
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-18: Satırın query'deki dizgiyi içerip içermediğini görmek için yeni bir işlevsellik ekleme

Şu anda işlevsellik geliştiriyoruz. Derlemek için, fonksiyon imzasında belirttiğimiz gibi gövdeden bir değer döndürmemiz gerekiyor.

Eşleşen Satırları Depolama

Bu fonksiyonu tamamlamak için, döndürmek istediğimiz eşleşen satırları saklamanın bir yoluna ihtiyacımız var. Bunun için, for döngüsünden önce değiştirilebilir bir vektör oluşturabilir ve bir satırı vektörde saklamak için push metodunu çağırabiliriz. for döngüsünden sonra, Liste 12-19'da gösterildiği gibi vektörü döndürürüz.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-19: Eşleşen satırları döndürebilmek için depolamak

Şimdi search fonksiyonu yalnızca query'i 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 [unoptimized + debuginfo] target(s) in 1.22s
     Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... ok

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

     Running unittests (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, bu yüzden işe yaradığını biliyoruz!

Bu noktada, aynı işlevselliği korumak için testlerin geçmesini sağlarken search fonksiyonunun süreklemesini yeniden düzenleme fırsatlarını değerlendirebiliriz. search fonksiyonundaki kod çok da kötü değil, ancak yineleyicilerin bazı yararlı özelliklerinden yararlanmıyor o kadar. Yineleyicileri ayrıntılı olarak inceleyeceğimiz Bölüm 13'te bu örneğe geri döneceğiz ve nasıl geliştirebileceğimize bakacağız.

search Fonksiyonunu run Fonksiyonunda Kullanma

Artık search fonksiyonu çalıştığına ve test edildiğine göre, run fonksiyonumuzdan search'ü çağırmamız gerekiyor. config.query değerini ve run'ın dosyadan okuduğu içeriği search fonksiyonuna aktarmamız gerekiyor. Ardından run, search çağrısından dönen her satırı yazdıracaktır:

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    for line in search(&config.query, &contents) {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Her satırı search'ten döndürmek ve yazdırmak için hala bir for döngüsü kullanıyoruz.

Artık programımız çalışmalıdır! İlk olarak Emily Dickinson şiirinden tam olarak bir dizgi döndürmesi gereken bir sözcük ile deneyelim, “frog”:

$ cargo run frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frog poem.txt`
How public, like a frog

Harika! Şimdi “body” gibi birden fazla satırla eşleşecek bir sözcüğü deneyelim:

$ cargo run body poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!

Ve son olarak, “monomorphization” gibi şiirin hiçbir yerinde olmayan bir sözcüğü aradığımızda herhangi bir satır almadığımızdan emin olalım:

$ cargo run monomorphization poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

Mükemmel! Klasik aracın kendi mini versiyonumuzu oluşturduk ve uygulamaların nasıl yapılandırılacağı hakkında çok şey öğrendik. Ayrıca dosya girişi ve çıkışı, yaşam süreleri, test etme ve komut satırı ayrıştırma hakkında da biraz bilgi edindik.

Bu projeyi tamamlamak için, her ikisi de komut satırı programları yazarken yararlı olan ortam değişkenleriyle nasıl çalışılacağını ve standart hataya nasıl yazdırılacağını kısaca göstereceğiz.