Writing a Failing Test
Because we don't need them anymore, let's remove the println!
statements from src/lib.rs
and src/main.rs
that we used to check the program's behavior. Then, in src/lib.rs
, we'll add a tests
module with a test function, as we did in Chapter 11. The test function specifies the behavior we want the search
function to have: it will take a query and the text to search, and it will return only the lines from the text that contain the query. Listing 12-15 shows this test, which won't compile yet.
Filename: src/lib.rs
#[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)
);
}
}
Listing 12-15: Creating a failing test for the search
function we wish we had
This test searches for the string "duct"
. The text we're searching is three lines, only one of which contains "duct"
(note that the backslash after the opening double quote tells Rust not to put a newline character at the beginning of the contents of this string literal). We assert that the value returned from the search
function contains only the line we expect.
We aren't yet able to run this test and watch it fail because the test doesn't even compile: the search
function doesn't exist yet! In accordance with TDD principles, we'll add just enough code to get the test to compile and run by adding a definition of the search
function that always returns an empty vector, as shown in Listing 12-16. Then the test should compile and fail because an empty vector doesn't match a vector containing the line "safe, fast, productive."
.
Filename: src/lib.rs
pub fn search<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
vec![]
}
Listing 12-16: Defining just enough of the search
function so our test will compile
Notice that we need to define an explicit lifetime 'a
in the signature of search
and use that lifetime with the contents
argument and the return value. Recall in Chapter 10 that the lifetime parameters specify which argument lifetime is connected to the lifetime of the return value. In this case, we indicate that the returned vector should contain string slices that reference slices of the argument contents
(rather than the argument query
).
In other words, we tell Rust that the data returned by the search
function will live as long as the data passed into the search
function in the contents
argument. This is important! The data referenced by a slice needs to be valid for the reference to be valid; if the compiler assumes we're making string slices of query
rather than contents
, it will do its safety checking incorrectly.
If we forget the lifetime annotations and try to compile this function, we'll get this error:
error[E0106]: missing lifetime specifier
--> src/lib.rs:31:10
|
29 | query: &str,
| ----
30 | contents: &str,
| ----
31 | ) -> 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>(
29 ~ query: &'a str,
30 ~ contents: &'a str,
31 ~ ) -> Vec<&'a str> {
|
Rust can't possibly know which of the two arguments we need, so we need to tell it explicitly. Because contents
is the argument that contains all of our text and we want to return the parts of that text that match, we know contents
is the argument that should be connected to the return value using the lifetime syntax.
Other programming languages don't require you to connect arguments to return values in the signature, but this practice will get easier over time. You might want to compare this example with the examples in "Validating References with Lifetimes".
Now let's run the test:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [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 'assertion failed: `(left == right)`
left: `["safe, fast, productive."]`,
right: `[]`', src/lib.rs:47: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'
Great, the test fails, exactly as we expected. Let's get the test to pass!