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:
- 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çmesi için yeterli kodu yazın veya 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ı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));
}
}
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));
}
}
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));
}
}
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));
}
}
Ş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));
}
}
Ş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.