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 logic tìm kiếm đã được tách ra khỏi hàm main
và nằm trong
src/lib.rs, việc viết kiểm thử cho chức năng cốt lõi của code trở nên dễ dàng
hơn rất nhiều. Chúng ta có thể gọi trực tiếp các hàm với nhiều đối số khác nhau
và kiểm tra giá trị trả về mà không cần phải chạy chương trình nhị phân của mình
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
Trong file src/lib.rs, chúng ta sẽ thêm một module tests
cùng với một hàm
kiểm thử, giống như cách chúng ta đã làm trong Chương 11. Hàm
kiểm thử này sẽ chỉ định hành vi mà chúng ta mong muốn hàm search
thực hiện:
nó sẽ nhận vào một chuỗi truy vấn (query) và đoạn văn bản cần tìm kiếm (text),
sau đó sẽ chỉ trả về những dòng từ văn bản đó mà có chứa chuỗi truy vấn. Listing
12-15 sẽ thể hiện bài kiểm thử này.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// --snip--
#[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.
Nếu chúng ta chạy bài kiểm thử này, nó sẽ thất bại hiện tại vì macro
unimplemented!
sẽ panic! với thông báo “not implemented”. Tuân thủ nguyên tắc
TDD (Test-Driven Development), chúng ta sẽ thực hiện một bước nhỏ: chỉ thêm đủ
code để bài kiểm thử không bị panic! khi gọi hàm bằng cách định nghĩa hàm
search
luôn trả về một vector rỗng, như trong Listing 12-16. Sau đó, bài kiểm
thử sẽ biên dịch được và vẫn thất bại vì một vector rỗng không khớp với vector
chứa dòng "safe, fast, productive."
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));
}
}
Bây giờ, chúng ta hãy thảo luận lý do tại sao chúng ta cần định nghĩa một
lifetime 'a
tường minh trong chữ ký của hàm search
và sử dụng lifetime đó
với đối số contents
cùng với giá trị trả về. Nhớ lại trong Chương
10 rằng các tham số lifetime chỉ định lifetime của đối số 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ề phải chứa các string slice tham chiếu đến các slice
của đối số contents
(chứ không phải đố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:1:51
|
1 | 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
|
1 | 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 biết được chúng ta cần tham số nào trong hai tham số, vì vậy chúng ta
cần phải cho nó biết rõ ràng. Lưu ý rằng văn bản trợ giúp có thể đề xuất chỉ
định cùng một tham số lifetime cho tất cả các tham số và kiểu dữ liệu trả về,
điều này là không chính xác! Bởi vì contents
là tham số chứa toàn bộ 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à tham số duy nhất 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.
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.
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.
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.
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ó.
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.