Skip to content

Commit 4de679a

Browse files
committed
Add "Developing Library Functionality with TDD" section to Chapter 12 and update SUMMARY.md
1 parent 95f588e commit 4de679a

7 files changed

Lines changed: 229 additions & 6 deletions

en/src/Chapter-12/12.1/12.1._Receiving_Command-Line_Arguments.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This project has several steps:
77
- Receive command-line arguments (this article)
88
- [Read files](../12.2/12.2._Read_Files.md)
99
- [Refactor: improve modules and error handling](../12.3/12.3._Refactoring_Pt.1_-_Improving_Modularity.md)
10-
- Use TDD (test-driven development) to develop library functionality
10+
- [Use TDD (test-driven development) to develop library functionality](../12.6/12.6._Developing_Library_Functionality_with_TDD.md)
1111
- Use environment variables
1212
- Write error messages to standard error instead of standard output
1313

en/src/Chapter-12/12.2/12.2._Read_Files.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This project has several steps:
77
- [Receive command-line arguments](../12.1/12.1._Receiving_Command-Line_Arguments.md)
88
- Read files (this article)
99
- [Refactor: improve modules and error handling](../12.3/12.3._Refactoring_Pt.1_-_Improving_Modularity.md)
10-
- Use TDD (test-driven development) to develop library functionality
10+
- [Use TDD (test-driven development) to develop library functionality](../12.6/12.6._Developing_Library_Functionality_with_TDD.md)
1111
- Use environment variables
1212
- Write error messages to standard error instead of standard output
1313

en/src/Chapter-12/12.3/12.3._Refactoring_Pt.1_-_Improving_Modularity.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This project has several steps:
77
- [Receive command-line arguments](../12.1/12.1._Receiving_Command-Line_Arguments.md)
88
- [Read files](../12.2/12.2._Read_Files.md)
99
- Refactor: improve modules and error handling (this article)
10-
- Use TDD (test-driven development) to develop library functionality
10+
- [Use TDD (test-driven development) to develop library functionality](../12.6/12.6._Developing_Library_Functionality_with_TDD.md)
1111
- Use environment variables
1212
- Write error messages to standard error instead of standard output
1313

en/src/Chapter-12/12.4/12.4._Refactoring_Pt.2_-_Error_Handling.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This project has several steps:
77
- [Receive command-line arguments](../12.1/12.1._Receiving_Command-Line_Arguments.md)
88
- [Read files](../12.2/12.2._Read_Files.md)
99
- Refactor: improve modules and error handling (this article)
10-
- Use TDD (test-driven development) to develop library functionality
10+
- [Use TDD (test-driven development) to develop library functionality](../12.6/12.6._Developing_Library_Functionality_with_TDD.md)
1111
- Use environment variables
1212
- Write error messages to standard error instead of standard output
1313

en/src/Chapter-12/12.5/12.5._Refactoring_Pt.3_-_Moving_Business_Logic.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This project has several steps:
77
- [Receive command-line arguments](../12.1/12.1._Receiving_Command-Line_Arguments.md)
88
- [Read files](../12.2/12.2._Read_Files.md)
99
- Refactor: improve modules and error handling (this article)
10-
- Use TDD (test-driven development) to develop library functionality
10+
- [Use TDD (test-driven development) to develop library functionality](../12.6/12.6._Developing_Library_Functionality_with_TDD.md)
1111
- Use environment variables
1212
- Write error messages to standard error instead of standard output
1313

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# 12.6 Developing Library Functionality with TDD
2+
3+
## 12.6.0 Before We Begin
4+
In Chapter 12, we will build a real project: a command-line program. This program is a `grep` (**Global Regular Expression Print**), a tool for global regular-expression search and output. Its job is to **search for the specified text in the specified file.**
5+
6+
This project has several steps:
7+
- [Receive command-line arguments](../12.1/12.1._Receiving_Command-Line_Arguments.md)
8+
- [Read files](../12.2/12.2._Read_Files.md)
9+
- [Refactor: improve modules and error handling](../12.3/12.3._Refactoring_Pt.1_-_Improving_Modularity.md)
10+
- Use TDD (test-driven development) to develop library functionality (this article)
11+
- Use environment variables
12+
- Write error messages to standard error instead of standard output
13+
14+
## 12.6.1 Review
15+
Here is all the code written up to the previous article.
16+
17+
`lib.rs`:
18+
```rust
19+
use std::error::Error;
20+
use std::fs;
21+
22+
pub struct Config {
23+
pub query: String,
24+
pub filename: String,
25+
}
26+
27+
impl Config {
28+
pub fn new(args: &[String]) -> Result<Config, &'static str> {
29+
if args.len() < 3 {
30+
return Err("Not enough arguments");
31+
}
32+
let query = args[1].clone();
33+
let filename = args[2].clone();
34+
Ok(Config { query, filename})
35+
}
36+
}
37+
38+
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
39+
let contents = fs::read_to_string(config.filename)?;
40+
println!("With text:\n{}", contents);
41+
Ok(())
42+
}
43+
```
44+
45+
`main.rs`:
46+
```rust
47+
use std::env;
48+
use std::process;
49+
use minigrep::Config;
50+
51+
fn main() {
52+
let args:Vec<String> = env::args().collect();
53+
let config = Config::new(&args).unwrap_or_else(|err| {
54+
println!("Problem parsing arguments: {}", err);
55+
process::exit(1);
56+
});
57+
if let Err(e) = minigrep::run(config) {
58+
println!("Application error: {}", e);
59+
process::exit(1);
60+
}
61+
}
62+
```
63+
64+
In the previous sections, we moved the business logic into `lib.rs`. That helps a lot with writing tests, because the logic in `lib.rs` can be called directly with different parameters without running the program from the command line, and we can verify its return values. In other words, we can test the business logic directly.
65+
66+
## 12.6.2 What Is Test-Driven Development?
67+
TDD stands for Test-Driven Development. It usually follows these steps:
68+
- Write a failing test, run it, and make sure it fails for the expected reason
69+
- Write or modify just enough code to make the new test pass
70+
- Refactor the code you just added or changed to make sure the tests still pass
71+
- Return to step 1 and continue
72+
73+
TDD is just one of many software development methods, but it can guide and help code design. Writing tests first and then writing code to pass those tests also helps maintain a high level of test coverage during development.
74+
75+
In this article, we will use TDD to implement the search logic: search for the specified string in the file contents and put the matching lines into a list. This function will be named `search`.
76+
77+
## 12.6.3 Modifying the Code
78+
Follow the TDD steps:
79+
80+
### 1. Write a Failing Test
81+
First, write a test module in `lib.rs`:
82+
```rust
83+
#[cfg(test)]
84+
mod tests {
85+
use super::*;
86+
87+
#[test]
88+
fn one_result() {
89+
let query = "duct";
90+
let contents = "\
91+
Rust:
92+
safe, fast, productive.
93+
Pick three.";
94+
assert_eq!(vec!["safe, fast, productive."],search(query, contents));
95+
}
96+
}
97+
```
98+
That is, because `"duct"` stored in `query` appears in the line `"safe, fast, productive."`, the return value should be a `Vector` whose element type is `String`, and it should contain only one element: `"safe, fast, productive."`.
99+
100+
The return value is a `Vector` because `search` is expected to handle multiple matching results. Of course, this particular test can only have one result, which is why the test is named `one_result`.
101+
102+
After writing the test module, write the `search` function:
103+
```rust
104+
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
105+
vec![]
106+
}
107+
```
108+
- To make the function callable from outside, it must be declared `pub`.
109+
- The function needs lifetime annotations because it has more than one non-`self` parameter, so Rust cannot tell which parameter’s lifetime matches the return value.
110+
- The elements in the returned `Vector` are string slices taken from `contents`, so the return value should have the same lifetime as `contents`. That is why both are annotated with the same lifetime `'a`, while `query` does not need a lifetime annotation.
111+
- The function body only needs to compile, because the first step of TDD is to write a failing test. Failure is the desired outcome right now.
112+
113+
The test will fail, and that is exactly what we want in the first step of TDD.
114+
115+
### 2. Write Just Enough Code for the New Test to Pass
116+
The code for `search_case_insensitive` is very similar to `search`; we only need a few changes. The logic is simple: convert both the query and the text to lowercase.
117+
```rust
118+
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
119+
let mut results = Vec::new();
120+
let query = query.to_lowercase();
121+
for line in contents.to_lowercase().lines() {
122+
if line.contains(&query) {
123+
results.push(line);
124+
}
125+
}
126+
results
127+
}
128+
```
129+
- The `to_lowercase` method converts a string to lowercase.
130+
- The result of `to_lowercase` is a `String`, which owns its data. That means the new `query` is a `String`, not a `&str`. In the `if` inside the loop, we use `&query` because `contains` does not accept `String`, so we must pass a reference.
131+
132+
Run the tests again:
133+
```
134+
$ cargo test
135+
Compiling minigrep v0.1.0 (file:///projects/minigrep)
136+
Finished `test` profile [unoptimized +debuginfo] target(s) in 1.33s
137+
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
138+
139+
running 2 tests
140+
test tests::case_insensitive ... ok
141+
test tests::case_sensitive ... ok
142+
143+
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
144+
145+
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
146+
147+
running 0 tests
148+
149+
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
150+
151+
Doc-tests minigrep
152+
153+
running 0 tests
154+
155+
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
156+
```
157+
Both tests pass.
158+
159+
### 3. Use This Function in `run`
160+
Now that the function works, we can call it from `run`.
161+
162+
But first, we need to add a field to the `Config` struct to decide whether to use the normal `search` or the case-insensitive `search_case_insensitive`:
163+
```rust
164+
pub struct Config {
165+
pub query: String,
166+
pub filename: String,
167+
pub case_sensitive: bool,
168+
}
169+
```
170+
171+
Update `run` so it checks the configuration:
172+
```rust
173+
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
174+
let contents = fs::read_to_string(config.filename)?;
175+
let results = if config.case_sensitive {
176+
search(&config.query, &contents)
177+
} else {
178+
search_case_insensitive(&config.query, &contents)
179+
};
180+
for line in results {
181+
println!("{}", line);
182+
}
183+
Ok(())
184+
}
185+
```
186+
187+
The `new` constructor on `Config` also needs to change, and it should set `case_sensitive` based on an environment variable:
188+
```rust
189+
impl Config {
190+
pub fn new(args: &[String]) -> Result<Config, &'static str> {
191+
if args.len() < 3 {
192+
return Err("Not enough arguments");
193+
}
194+
let query = args[1].clone();
195+
let filename = args[2].clone();
196+
let case_sensitive = std::env::var("CASE_INSENSITIVE").is_err();
197+
Ok(Config { query, filename, case_sensitive})
198+
}
199+
}
200+
```
201+
This uses `std::env::var` (you can also import `std::env` first and then use `env::var`). Its argument is the name of the environment variable, usually written in all caps. Here I use `CASE_INSENSITIVE`, which means “case-insensitive.” If this environment variable exists, we treat the search as case-insensitive; if it does not exist, we treat it as case-sensitive.
202+
203+
`std::env::var` returns a `Result`. If `CASE_INSENSITIVE` is set, it returns `Ok(String)` containing the variable’s value; otherwise it returns `Err(std::env::VarError)`.
204+
205+
The `is_err` method is chained after `std::env::var`. If the result is `Err`, it returns `true` and assigns `true` to `case_sensitive`; otherwise it assigns `false`.
206+
207+
## 12.6.3 The Full Code and a Trial Run
208+
Here is all the code written so far.
209+
210+
`lib.rs`:
211+
```rust
212+
use std::error::Error;
213+
use std::fs;
214+
215+
pub struct Config {
216+
pub query: String,
217+
pub filename: String,
218+
pub case_sensitive: bool,
219+
}
220+
221+
impl Config {
222+
```

en/src/SUMMARY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@
6464
- [Read Files](./Chapter-12/12.2/12.2._Read_Files.md)
6565
- [Refactoring Pt.1 - Improving Modularity](./Chapter-12/12.3/12.3._Refactoring_Pt.1_-_Improving_Modularity.md)
6666
- [Refactoring Pt.2 - Error Handling](./Chapter-12/12.4/12.4._Refactoring_Pt.2_-_Error_Handling.md)
67-
- [Refactoring Pt.3 - Moving Business Logic](./Chapter-12/12.5/12.5._Refactoring_Pt.3_-_Moving_Business_Logic.md)
67+
- [Refactoring Pt.3 - Moving Business Logic](./Chapter-12/12.5/12.5._Refactoring_Pt.3_-_Moving_Business_Logic.md)
68+
- [Developing Library Functionality with TDD](./Chapter-12/12.6/12.6._Developing_Library_Functionality_with_TDD.md)

0 commit comments

Comments
 (0)