Phát triển chức năng của thư viện với phát triển hướng kiểm thử
Giờ đây khi chúng ta đã trích xuất logic vào src/lib.rs và để lại phần thu thập đối số và xử lý lỗi trong src/main.rs, việc viết các bài kiểm thử cho chức năng cốt lõi của mã của chúng ta trở nên dễ dàng hơn nhiều. Chúng ta có thể gọi các hàm trực tiếp với các đối số khác nhau và kiểm tra giá trị trả về mà không cần phải gọi nhị phân của chúng ta từ dòng lệnh.
Trong phần này, chúng ta sẽ thêm logic tìm kiếm vào chương trình minigrep
bằng
cách sử dụng quy trình phát triển hướng kiểm thử (TDD) với các bước sau:
- Viết một bài kiểm thử thất bại và chạy nó để đảm bảo rằng nó thất bại vì lý do bạn mong đợi.
- Viết hoặc chỉnh sửa đủ mã để làm cho bài kiểm thử mới vượt qua.
- Cải tiến mã bạn vừa thêm hoặc thay đổi và đảm bảo các bài kiểm thử tiếp tục vượt qua.
- Lặp lại từ bước 1!
Mặc dù đây chỉ là một trong nhiều cách để viết phần mềm, TDD có thể giúp định hướng thiết kế mã. Viết bài kiểm thử trước khi bạn viết mã làm cho bài kiểm thử vượt qua giúp duy trì độ phủ kiểm thử cao trong suốt quá trình.
Chúng ta sẽ kiểm tra-hướng việc triển khai chức năng thực sự tìm kiếm chuỗi truy
vấn trong nội dung tệp và tạo ra danh sách các dòng phù hợp với truy vấn. Chúng
ta sẽ thêm chức năng này trong một hàm gọi là search
.
Viết một bài kiểm thử thất bại
Bởi vì chúng ta không còn cần chúng nữa, hãy xóa các câu lệnh println!
từ
src/lib.rs và src/main.rs mà chúng ta đã sử dụng để kiểm tra hành vi của
chương trình. Sau đó, trong src/lib.rs, chúng ta sẽ thêm một mô-đun tests
với một hàm kiểm thử, như chúng ta đã làm trong Chương
11. Hàm kiểm thử chỉ định hành vi mà chúng ta muốn
hàm search
có: nó sẽ lấy một truy vấn và văn bản để tìm kiếm, và nó sẽ chỉ trả
về các dòng từ văn bản mà chứa truy vấn. Listing 12-15 hiển thị bài kiểm thử
này, mà chưa thể biên dịch được.
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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));
}
}
Bài kiểm thử này tìm kiếm chuỗi "duct"
. Văn bản chúng ta đang tìm kiếm có ba
dòng, chỉ có một dòng chứa "duct"
(lưu ý rằng dấu gạch chéo ngược sau dấu
ngoặc kép mở báo với Rust không đặt ký tự xuống dòng vào đầu nội dung của chuỗi
chữ này). Chúng ta khẳng định rằng giá trị trả về từ hàm search
chỉ chứa dòng
mà chúng ta mong đợi.
Chúng ta chưa thể chạy bài kiểm thử này và xem nó thất bại vì bài kiểm thử thậm
chí không biên dịch được: hàm search
chưa tồn tại! Theo các nguyên tắc TDD,
chúng ta sẽ thêm đủ mã để bài kiểm thử biên dịch và chạy bằng cách thêm một định
nghĩa của hàm search
luôn trả về một vector rỗng, như được hiển thị trong
Listing 12-16. Sau đó bài kiểm thử sẽ biên dịch và thất bại vì một vector rỗng
không khớp với vector chứa dòng "safe, fast, productive."
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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));
}
}
Lưu ý rằng chúng ta cần định nghĩa một lifetime rõ ràng 'a
trong chữ ký của
search
và sử dụng lifetime đó với đối số contents
và giá trị trả về. Nhớ lại
trong Chương 10 rằng tham số lifetime chỉ định
đối số lifetime nào được kết nối với lifetime của giá trị trả về. Trong trường
hợp này, chúng ta chỉ ra rằng vector được trả về sẽ chứa slice chuỗi tham chiếu
đến các slice của đối số contents
(thay vì đối số query
).
Nói cách khác, chúng ta nói với Rust rằng dữ liệu được trả về bởi hàm search
sẽ tồn tại lâu như dữ liệu được truyền vào hàm search
trong đối số contents
.
Điều này quan trọng! Dữ liệu được tham chiếu bởi một slice cần phải hợp lệ để
tham chiếu hợp lệ; nếu trình biên dịch giả định chúng ta đang tạo slice chuỗi
của query
thay vì contents
, nó sẽ thực hiện kiểm tra an toàn của nó không
chính xác.
Nếu chúng ta quên chú thích lifetime và cố gắng biên dịch hàm này, chúng ta sẽ gặp lỗi này:
$ 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` (lib) due to 1 previous error
Rust không thể biết được chúng ta cần đối số nào trong hai đối số, vì vậy chúng
ta cần phải cho nó biết rõ ràng. Bởi vì contents
là đối số chứa tất cả văn bản
của chúng ta và chúng ta muốn trả về các phần của văn bản đó phù hợp, chúng ta
biết contents
là đối số nên được kết nối với giá trị trả về bằng cách sử dụng
cú pháp lifetime.
Các ngôn ngữ lập trình khác không yêu cầu bạn kết nối các đối số với giá trị trả về trong chữ ký, nhưng thực hành này sẽ trở nên dễ dàng hơn theo thời gian. Bạn có thể muốn so sánh ví dụ này với các ví dụ trong phần "Xác thực tham chiếu với Lifetime" trong Chương 10.
Bây giờ hãy chạy bài kiểm thử:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.97s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... FAILED
failures:
---- tests::one_result stdout ----
thread 'tests::one_result' panicked at src/lib.rs:44:9:
assertion `left == right` failed
left: ["safe, fast, productive."]
right: []
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`
Tuyệt, bài kiểm thử thất bại, chính xác như chúng ta mong đợi. Hãy làm cho bài kiểm thử vượt qua!
Viết mã để vượt qua bài kiểm thử
Hiện tại, bài kiểm thử của chúng ta đang thất bại vì chúng ta luôn trả về một
vector rỗng. Để sửa điều đó và triển khai search
, chương trình của chúng ta
cần tuân theo các bước sau:
- Lặp qua từng dòng của nội dung.
- Kiểm tra xem dòng đó có chứa chuỗi truy vấn của chúng ta hay không.
- Nếu có, thêm nó vào danh sách các giá trị mà chúng ta đang trả về.
- Nếu không, không làm gì cả.
- Trả về danh sách các kết quả phù hợp.
Hãy làm việc qua từng bước, bắt đầu với việc lặp qua các dòng.
Lặp qua các dòng với phương thức lines
Rust có một phương thức hữu ích để xử lý lặp từng dòng của chuỗi, tiện lợi được
đặt tên là lines
, hoạt động như được hiển thị trong Listing 12-17. Lưu ý rằng
điều này chưa thể biên dịch được.
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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));
}
}
Phương thức lines
trả về một iterator. Chúng ta sẽ nói về iterator một cách
chi tiết trong Chương 13, nhưng hãy nhớ lại
rằng bạn đã thấy cách sử dụng iterator này trong Listing
3-5, nơi chúng ta đã sử dụng vòng lặp for
với một
iterator để chạy một đoạn mã trên mỗi phần tử trong một bộ sưu tập.
Tìm kiếm từng dòng cho truy vấn
Tiếp theo, chúng ta sẽ kiểm tra xem dòng hiện tại có chứa chuỗi truy vấn của
chúng ta hay không. May mắn thay, chuỗi có một phương thức hữu ích có tên là
contains
làm điều này cho chúng ta! Thêm một lệnh gọi đến phương thức
contains
trong hàm search
, như được hiển thị trong Listing 12-18. Lưu ý rằng
điều này vẫn chưa biên dịch được.
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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));
}
}
Hiện tại, chúng ta đang xây dựng chức năng. Để mã biên dịch, chúng ta cần trả về một giá trị từ thân hàm như chúng ta đã chỉ ra trong chữ ký hàm.
Lưu trữ các dòng phù hợp
Để hoàn thành hàm này, chúng ta cần một cách để lưu trữ các dòng phù hợp mà
chúng ta muốn trả về. Để làm điều đó, chúng ta có thể tạo một vector có thể thay
đổi trước vòng lặp for
và gọi phương thức push
để lưu trữ một line
trong
vector. Sau vòng lặp for
, chúng ta trả về vector, như được hiển thị trong
Listing 12-19.
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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));
}
}
Bây giờ hàm search
chỉ nên trả về các dòng chứa query
, và bài kiểm thử của
chúng ta sẽ vượt qua. Hãy chạy bài kiểm thử:
$ 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::one_result ... 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
Bài kiểm thử của chúng ta đã vượt qua, vì vậy chúng ta biết nó hoạt động!
Tại thời điểm này, chúng ta có thể xem xét các cơ hội để cải tiến việc triển khai hàm tìm kiếm trong khi giữ cho các bài kiểm thử vượt qua để duy trì chức năng tương tự. Mã trong hàm tìm kiếm không quá tệ, nhưng nó không tận dụng một số tính năng hữu ích của iterator. Chúng ta sẽ quay lại ví dụ này trong Chương 13, nơi chúng ta sẽ khám phá iterator một cách chi tiết và xem xét cách cải thiện nó.
Sử dụng hàm search
trong hàm run
Bây giờ hàm search
đang hoạt động và được kiểm thử, chúng ta cần gọi search
từ hàm run
của chúng ta. Chúng ta cần truyền giá trị config.query
và
contents
mà run
đọc từ tệp cho hàm search
. Sau đó run
sẽ in mỗi dòng
được trả về từ search
:
Tên tệp: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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));
}
}
Chúng ta vẫn đang sử dụng vòng lặp for
để trả về mỗi dòng từ search
và in
nó.
Bây giờ toàn bộ chương trình sẽ hoạt động! Hãy thử nó, đầu tiên với một từ nên trả về chính xác một dòng từ bài thơ của Emily Dickinson: frog.
$ cargo run -- frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
Tuyệt! Bây giờ hãy thử một từ sẽ khớp với nhiều dòng, như body:
$ cargo run -- body poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [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!
Và cuối cùng, hãy đảm bảo rằng chúng ta không nhận được bất kỳ dòng nào khi chúng ta tìm kiếm một từ không có ở bất kỳ đâu trong bài thơ, ví dụ như monomorphization:
$ cargo run -- monomorphization poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
Xuất sắc! Chúng ta đã xây dựng phiên bản mini của riêng mình cho một công cụ cổ điển và đã học được rất nhiều về cách cấu trúc ứng dụng. Chúng ta cũng đã học được một chút về đầu vào và đầu ra tệp, lifetime, kiểm thử và phân tích dòng lệnh.
Để hoàn thành dự án này, chúng ta sẽ trình bày ngắn gọn cách làm việc với biến môi trường và cách in ra lỗi tiêu chuẩn, cả hai đều hữu ích khi bạn đang viết các chương trình dòng lệnh.