]>
Commit | Line | Data |
---|---|---|
13cf67c4 XL |
1 | ## Developing the Library’s Functionality with Test-Driven Development |
2 | ||
3 | Now that we’ve extracted the logic into *src/lib.rs* and left the argument | |
4 | collecting and error handling in *src/main.rs*, it’s much easier to write tests | |
5 | for the core functionality of our code. We can call functions directly with | |
6 | various arguments and check return values without having to call our binary | |
7 | from the command line. Feel free to write some tests for the functionality in | |
8 | the `Config::new` and `run` functions on your own. | |
9 | ||
10 | In this section, we’ll add the searching logic to the `minigrep` program by | |
11 | using the Test-driven development (TDD) process. This software development | |
12 | technique follows these steps: | |
13 | ||
14 | 1. Write a test that fails and run it to make sure it fails for the reason you | |
15 | expect. | |
16 | 2. Write or modify just enough code to make the new test pass. | |
17 | 3. Refactor the code you just added or changed and make sure the tests | |
18 | continue to pass. | |
19 | 4. Repeat from step 1! | |
20 | ||
21 | This process is just one of many ways to write software, but TDD can help drive | |
22 | code design as well. Writing the test before you write the code that makes the | |
23 | test pass helps to maintain high test coverage throughout the process. | |
24 | ||
25 | We’ll test drive the implementation of the functionality that will actually do | |
26 | the searching for the query string in the file contents and produce a list of | |
27 | lines that match the query. We’ll add this functionality in a function called | |
28 | `search`. | |
29 | ||
30 | ### Writing a Failing Test | |
31 | ||
32 | Because we don’t need them anymore, let’s remove the `println!` statements from | |
33 | *src/lib.rs* and *src/main.rs* that we used to check the program’s behavior. | |
34 | Then, in *src/lib.rs*, we’ll add a `tests` module with a test function, as we | |
48663c56 XL |
35 | did in [Chapter 11][ch11-anatomy]<!-- ignore -->. The test function specifies |
36 | the behavior we want the `search` function to have: it will take a query and | |
37 | the text to search for the query in, and it will return only the lines from the | |
38 | text that contain the query. Listing 12-15 shows this test, which won’t compile | |
39 | yet. | |
13cf67c4 XL |
40 | |
41 | <span class="filename">Filename: src/lib.rs</span> | |
42 | ||
74b04a01 XL |
43 | ```rust,ignore,does_not_compile |
44 | {{#rustdoc_include ../listings/ch12-an-io-project/listing-12-15/src/lib.rs:here}} | |
13cf67c4 XL |
45 | ``` |
46 | ||
47 | <span class="caption">Listing 12-15: Creating a failing test for the `search` | |
48 | function we wish we had</span> | |
49 | ||
50 | This test searches for the string `"duct"`. The text we’re searching is three | |
136023e0 XL |
51 | lines, only one of which contains `"duct"` (Note that the backslash after the |
52 | opening double quote tells Rust not to put a newline character at the beginning | |
53 | of the contents of this string literal). We assert that the value returned from | |
54 | the `search` function contains only the line we expect. | |
13cf67c4 XL |
55 | |
56 | We aren’t able to run this test and watch it fail because the test doesn’t even | |
57 | compile: the `search` function doesn’t exist yet! So now we’ll add just enough | |
58 | code to get the test to compile and run by adding a definition of the `search` | |
59 | function that always returns an empty vector, as shown in Listing 12-16. Then | |
60 | the test should compile and fail because an empty vector doesn’t match a vector | |
61 | containing the line `"safe, fast, productive."` | |
62 | ||
63 | <span class="filename">Filename: src/lib.rs</span> | |
64 | ||
fc512014 | 65 | ```rust,noplayground |
74b04a01 | 66 | {{#rustdoc_include ../listings/ch12-an-io-project/listing-12-16/src/lib.rs:here}} |
13cf67c4 XL |
67 | ``` |
68 | ||
69 | <span class="caption">Listing 12-16: Defining just enough of the `search` | |
70 | function so our test will compile</span> | |
71 | ||
72 | Notice that we need an explicit lifetime `'a` defined in the signature of | |
73 | `search` and used with the `contents` argument and the return value. Recall in | |
48663c56 XL |
74 | [Chapter 10][ch10-lifetimes]<!-- ignore --> that the lifetime parameters |
75 | specify which argument lifetime is connected to the lifetime of the return | |
76 | value. In this case, we indicate that the returned vector should contain string | |
77 | slices that reference slices of the argument `contents` (rather than the | |
78 | argument `query`). | |
13cf67c4 XL |
79 | |
80 | In other words, we tell Rust that the data returned by the `search` function | |
81 | will live as long as the data passed into the `search` function in the | |
82 | `contents` argument. This is important! The data referenced *by* a slice needs | |
83 | to be valid for the reference to be valid; if the compiler assumes we’re making | |
84 | string slices of `query` rather than `contents`, it will do its safety checking | |
85 | incorrectly. | |
86 | ||
87 | If we forget the lifetime annotations and try to compile this function, we’ll | |
88 | get this error: | |
89 | ||
f035d41b | 90 | ```console |
74b04a01 | 91 | {{#include ../listings/ch12-an-io-project/output-only-02-missing-lifetimes/output.txt}} |
13cf67c4 XL |
92 | ``` |
93 | ||
94 | Rust can’t possibly know which of the two arguments we need, so we need to tell | |
95 | it. Because `contents` is the argument that contains all of our text and we | |
96 | want to return the parts of that text that match, we know `contents` is the | |
97 | argument that should be connected to the return value using the lifetime syntax. | |
98 | ||
99 | Other programming languages don’t require you to connect arguments to return | |
9fa01778 XL |
100 | values in the signature. Although this might seem strange, it will get easier |
101 | over time. You might want to compare this example with the [“Validating | |
102 | References with Lifetimes”][validating-references-with-lifetimes]<!-- ignore | |
103 | --> section in Chapter 10. | |
13cf67c4 XL |
104 | |
105 | Now let’s run the test: | |
106 | ||
f035d41b | 107 | ```console |
74b04a01 | 108 | {{#include ../listings/ch12-an-io-project/listing-12-16/output.txt}} |
13cf67c4 XL |
109 | ``` |
110 | ||
111 | Great, the test fails, exactly as we expected. Let’s get the test to pass! | |
112 | ||
113 | ### Writing Code to Pass the Test | |
114 | ||
115 | Currently, our test is failing because we always return an empty vector. To fix | |
116 | that and implement `search`, our program needs to follow these steps: | |
117 | ||
118 | * Iterate through each line of the contents. | |
119 | * Check whether the line contains our query string. | |
120 | * If it does, add it to the list of values we’re returning. | |
121 | * If it doesn’t, do nothing. | |
122 | * Return the list of results that match. | |
123 | ||
124 | Let’s work through each step, starting with iterating through lines. | |
125 | ||
126 | #### Iterating Through Lines with the `lines` Method | |
127 | ||
128 | Rust has a helpful method to handle line-by-line iteration of strings, | |
129 | conveniently named `lines`, that works as shown in Listing 12-17. Note this | |
9fa01778 | 130 | won’t compile yet. |
13cf67c4 XL |
131 | |
132 | <span class="filename">Filename: src/lib.rs</span> | |
133 | ||
6a06907d | 134 | ```rust,ignore,does_not_compile |
74b04a01 | 135 | {{#rustdoc_include ../listings/ch12-an-io-project/listing-12-17/src/lib.rs:here}} |
13cf67c4 XL |
136 | ``` |
137 | ||
138 | <span class="caption">Listing 12-17: Iterating through each line in `contents` | |
139 | </span> | |
140 | ||
141 | The `lines` method returns an iterator. We’ll talk about iterators in depth in | |
e74abb32 | 142 | [Chapter 13][ch13-iterators]<!-- ignore -->, but recall that you saw this way of using an |
48663c56 XL |
143 | iterator in [Listing 3-5][ch3-iter]<!-- ignore -->, where we used a `for` loop |
144 | with an iterator to run some code on each item in a collection. | |
13cf67c4 XL |
145 | |
146 | #### Searching Each Line for the Query | |
147 | ||
148 | Next, we’ll check whether the current line contains our query string. | |
149 | Fortunately, strings have a helpful method named `contains` that does this for | |
150 | us! Add a call to the `contains` method in the `search` function, as shown in | |
9fa01778 | 151 | Listing 12-18. Note this still won’t compile yet. |
13cf67c4 XL |
152 | |
153 | <span class="filename">Filename: src/lib.rs</span> | |
154 | ||
6a06907d | 155 | ```rust,ignore,does_not_compile |
74b04a01 | 156 | {{#rustdoc_include ../listings/ch12-an-io-project/listing-12-18/src/lib.rs:here}} |
13cf67c4 XL |
157 | ``` |
158 | ||
159 | <span class="caption">Listing 12-18: Adding functionality to see whether the | |
160 | line contains the string in `query`</span> | |
161 | ||
162 | #### Storing Matching Lines | |
163 | ||
164 | We also need a way to store the lines that contain our query string. For that, | |
165 | we can make a mutable vector before the `for` loop and call the `push` method | |
166 | to store a `line` in the vector. After the `for` loop, we return the vector, as | |
9fa01778 | 167 | shown in Listing 12-19. |
13cf67c4 XL |
168 | |
169 | <span class="filename">Filename: src/lib.rs</span> | |
170 | ||
171 | ```rust,ignore | |
74b04a01 | 172 | {{#rustdoc_include ../listings/ch12-an-io-project/listing-12-19/src/lib.rs:here}} |
13cf67c4 XL |
173 | ``` |
174 | ||
175 | <span class="caption">Listing 12-19: Storing the lines that match so we can | |
176 | return them</span> | |
177 | ||
178 | Now the `search` function should return only the lines that contain `query`, | |
179 | and our test should pass. Let’s run the test: | |
180 | ||
f035d41b | 181 | ```console |
74b04a01 | 182 | {{#include ../listings/ch12-an-io-project/listing-12-19/output.txt}} |
13cf67c4 XL |
183 | ``` |
184 | ||
185 | Our test passed, so we know it works! | |
186 | ||
187 | At this point, we could consider opportunities for refactoring the | |
188 | implementation of the search function while keeping the tests passing to | |
189 | maintain the same functionality. The code in the search function isn’t too bad, | |
190 | but it doesn’t take advantage of some useful features of iterators. We’ll | |
e74abb32 | 191 | return to this example in [Chapter 13][ch13-iterators]<!-- ignore -->, where we’ll |
48663c56 | 192 | explore iterators in detail, and look at how to improve it. |
13cf67c4 XL |
193 | |
194 | #### Using the `search` Function in the `run` Function | |
195 | ||
196 | Now that the `search` function is working and tested, we need to call `search` | |
197 | from our `run` function. We need to pass the `config.query` value and the | |
198 | `contents` that `run` reads from the file to the `search` function. Then `run` | |
199 | will print each line returned from `search`: | |
200 | ||
201 | <span class="filename">Filename: src/lib.rs</span> | |
202 | ||
203 | ```rust,ignore | |
74b04a01 | 204 | {{#rustdoc_include ../listings/ch12-an-io-project/no-listing-02-using-search-in-run/src/lib.rs:here}} |
13cf67c4 XL |
205 | ``` |
206 | ||
207 | We’re still using a `for` loop to return each line from `search` and print it. | |
208 | ||
209 | Now the entire program should work! Let’s try it out, first with a word that | |
210 | should return exactly one line from the Emily Dickinson poem, “frog”: | |
211 | ||
f035d41b | 212 | ```console |
74b04a01 | 213 | {{#include ../listings/ch12-an-io-project/no-listing-02-using-search-in-run/output.txt}} |
13cf67c4 XL |
214 | ``` |
215 | ||
216 | Cool! Now let’s try a word that will match multiple lines, like “body”: | |
217 | ||
f035d41b | 218 | ```console |
74b04a01 | 219 | {{#include ../listings/ch12-an-io-project/output-only-03-multiple-matches/output.txt}} |
13cf67c4 XL |
220 | ``` |
221 | ||
222 | And finally, let’s make sure that we don’t get any lines when we search for a | |
223 | word that isn’t anywhere in the poem, such as “monomorphization”: | |
224 | ||
f035d41b | 225 | ```console |
74b04a01 | 226 | {{#include ../listings/ch12-an-io-project/output-only-04-no-matches/output.txt}} |
13cf67c4 XL |
227 | ``` |
228 | ||
229 | Excellent! We’ve built our own mini version of a classic tool and learned a lot | |
230 | about how to structure applications. We’ve also learned a bit about file input | |
231 | and output, lifetimes, testing, and command line parsing. | |
232 | ||
233 | To round out this project, we’ll briefly demonstrate how to work with | |
234 | environment variables and how to print to standard error, both of which are | |
235 | useful when you’re writing command line programs. | |
9fa01778 XL |
236 | |
237 | [validating-references-with-lifetimes]: | |
238 | ch10-03-lifetime-syntax.html#validating-references-with-lifetimes | |
48663c56 XL |
239 | [ch11-anatomy]: ch11-01-writing-tests.html#the-anatomy-of-a-test-function |
240 | [ch10-lifetimes]: ch10-03-lifetime-syntax.html | |
241 | [ch3-iter]: ch03-05-control-flow.html#looping-through-a-collection-with-for | |
e74abb32 | 242 | [ch13-iterators]: ch13-02-iterators.html |