Hey,
Before I proceed, I want to say thanks to @bjz for proof-reading this post and getting me started in Rust! He’s said that @steveklabnik and @carols10cents are working hard on a rewrite of the book, so it would be great to hear how this fits in with the new direction.
I’ve started looking at Rust and am very impressed by the type system. The Rust Book has been great and explains a lot of the language concepts.
As somebody who comes from both static and dynamic languages backgrounds with a heavy focus on TDD (esp in dynamic world), I noticed though that testing is only introduced later in a different section as a side-note.
It would be great to see how types and testing can be used together in the development process from the start (via test-first and type-first approaches). The fact that Rust has a unit testing framework built-in means that we have a great opportunity to develop this culture. This fits in with Rust’s goals of focusing on quality and safety.
With that in mind I would like to hear what others think and suggest the following to start with:
Start the book with the basics of testing
It is really great that Rust comes with built-in, basic testing framework. After explaining bare minimum, we could introduce the test framework to be used in most other parts of the Rust Book.
We could explain few basics without diving into the details:
- how to run the tests (
cargo test
) - structure (
[cfg(test)]
,[test]
,super::
should be enough) - no need to mention integration tests at the beginning yet
- provide a more detailed section on testing, past the basics (reference quickcheck and other tools)
Supplement the Rust Book with appropriate test code
Probably most of the code, starting from 4.5 (if), can use tests instead of println!
.
For example, instead of:
let x = 5;
if x == 5 {
println!("x is five!");
} else if x == 6 {
println!("x is six!");
} else {
println!("x is not five or six :(");
}
We could offer something like:
fn if_example() -> &'static str {
let x = 5;
if x == 5 {
"x is five!"
} else if x == 6 {
"x is six!"
} else {
"x is not five or six :("
}
}
# [cfg(test)]
mod tests {
# [test]
fn if_example_returns_five() {
assert_eq!(super::if_example(), "x is five");
}
}
Then as the examples get more complex, the test-driven book would be even more useful as the readers would be able to think about structuring the code better too.
This would advocate experimentation through the automated testing, which will naturally transition over into the systems that Rust noobs like me would start working on.
Advocate the quality through automated testing in the Guessing Game
Currently the Guessing Game is a showcase of the Rust capabilities which is awesome. But the way I see it - it is 30+ lines of production code without a single test.
It is great to showcase it, which I imagine is the main purpose. But it isn’t great at building the quality into the culture of Rust Engineers’ minds.
If we will try to test it, it basically becomes impossible without a good refactoring (and we can’t refactor without tests).
So a better example would be to structure the Guessing Game with testing in mind and maybe apply some of the TDD techniques (or other test approaches).
For example, with testing in mind, we could implement some tests first.
But we’d have to think about the types to even compile, so we’d start with:
enum GuessResult {
Smaller,
Bigger,
Guessed,
BadInput
}
then try to write some unit tests for our system:
// Start with some tests
# [cfg(test)]
mod tests {
use super::{game_over, GuessResult};
# [test]
fn game_over_is_true_when_guessed() {
assert_eq!(game_over(GuessResult::Guessed), true);
}
# [test]
fn game_over_is_false_if_not_guessed() {
assert_eq!(game_over(GuessResult::Smaller), false);
assert_eq!(game_over(GuessResult::Bigger), false);
assert_eq!(game_over(GuessResult::BadInput), false);
}
# [test]
fn process_guess_returns_correct_guess_result() {
// ...
}
# [test]
fn print_guess_prints_formatted_result() {
// ...
}
}
Then use unimplemented!()
as a placeholder. And then start implementing it.
// parts are ommitted...
fn process_guess(line: std::io::Result<String>, number_to_guess: i32) -> GuessResult {
unimplemented!()
}
fn print_guess(result: GuessResult) -> GuessResult {
unimplemented!()
}
fn game_over(result: &GuessResult) -> bool {
unimplemented!()
}
fn main() {
let input = io::stdin();
let lock = input.lock();
let number_to_guess = rand::thread_rng().gen_range(1, 1001);
let guesses = lock.lines()
.map(|line| process_guess(line, number_to_guess))
.map(|guess_result| print_guess(guess_result))
.take_while(|guess_result| !game_over(guess_result));
println!("Attempts: {}", guesses.count());
}
This, while not fully tested, is a big step towards it. Later on it can be covered with an integration test too, as a bit more advanced subject.
UPDATE: my noob attempt at writing the Guess Game in a more testable way.
Looking forward to hearing what other think on this matter!