Last active
December 11, 2020 11:56
-
-
Save spinnylights/02308de189f7330da595fcdff394426c to your computer and use it in GitHub Desktop.
Rust book "minigrep" project: test doubles + spies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This approach is based on the one devised in | |
// http://xion.io/post/programming/gisht-recap.html by Karol | |
// Kuczmarski; it permits a nice, easy-to-use API while still | |
// allowing you to inject doubles behind the scenes. | |
// | |
// As an example, this is in main.rs: | |
// | |
// let mut grepper = Grepper::new(config.filename); | |
// if let Err(e) = grepper.run() { | |
// ... | |
// | |
// The basic idea is to have a wrapper type (Grepper, in this | |
// case) that we parameterize in order to permit test doubles to | |
// be injected into it. We store this parameterized definition in | |
// a submodule. We then shadow it with a type alias in the outer | |
// module with the concrete types we want to expose to the | |
// public, along with a public constructor that instantiates | |
// those types. Our tests create instances of the wrapper type | |
// via literals, where they can use their doubles. | |
// | |
// This keeps all the testing infrastructure from leaking out | |
// into the public API and making it awkward to use. At the same | |
// time, it allows for tests to easily keep track of their | |
// doubles in case they want to use them as spies. Doubles can be | |
// arbitrarily complex, and new ones can be defined as needed | |
// without interefering with the others. Only vanilla Rust is | |
// used; no crate support is needed. | |
// Config stuff... | |
/// A trait for reading in text. | |
pub trait Reader { | |
fn read(&mut self) -> io::Result<String>; | |
} | |
/// A Reader that reads in the text of a file. | |
/// | |
/// This is automatically used by Greppers created from outside | |
/// this module. | |
pub struct FSReader { | |
filename: String, | |
} | |
impl Reader for FSReader { | |
fn read(&mut self) -> io::Result<String> { | |
fs::read_to_string(self.filename.clone()) | |
} | |
} | |
/// A trait for outputting text. | |
pub trait Printer { | |
fn print(&mut self, text: String) -> (); | |
} | |
/// A Printer that writes to stdout. | |
/// | |
/// This is automatically used by Greppers created from outside | |
/// this module. | |
pub struct StdoutPrinter; | |
impl Printer for StdoutPrinter { | |
fn print(&mut self, text: String) -> () { | |
println!("With text:\n{}", text); | |
} | |
} | |
// This hides the parameterization of Grepper from the outside | |
// world, making it appear to users as a concrete type that | |
// just does the work they want. Users can simply instantiate | |
// a Grepper via Grepper::new("filename") and don't have to go | |
// through any ceremony to accomodate the testing | |
// infrastructure. | |
// | |
/// Takes in the text of a file and writes it to stdout. | |
pub type Grepper = internal::Grepper<FSReader, StdoutPrinter>; | |
impl Grepper { | |
pub fn new(filename: String) -> Self { | |
internal::Grepper{ | |
reader: FSReader { filename }, | |
printer: StdoutPrinter, | |
} | |
} | |
} | |
mod internal { | |
use std::error::Error; | |
use super::{Reader, Printer}; | |
/// The parameterized Grepper type, which can have test | |
/// doubles injected. | |
pub struct Grepper<R: Reader, P: Printer> { | |
pub(super) reader: R, | |
pub(super) printer: P, | |
} | |
impl<R: Reader, P: Printer> Grepper<R, P> { | |
// Doesn't actually grep yet at this point in the | |
// tutorial. | |
// | |
/// Read in from the reader, write out from the printer. | |
pub fn run(&mut self) -> Result<(), Box<dyn Error>> { | |
let contents = self.reader.read()?; | |
self.printer.print(contents); | |
Ok(()) | |
} | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
// Config::new tests... | |
// Has a stubbed-out print() for testing purposes. | |
struct DummyPrinter; | |
impl Printer for DummyPrinter { | |
fn print(&mut self, _: String) -> () {} | |
} | |
// Simulates a successful file read. | |
struct OKTestReader; | |
impl Reader for OKTestReader { | |
fn read(&mut self) -> io::Result<String> { | |
Ok(String::from("contents of file")) | |
} | |
} | |
#[test] | |
fn test_run_works_with_valid_contents() { | |
let mut grepper = internal::Grepper { | |
reader: OKTestReader, | |
printer: DummyPrinter, | |
}; | |
assert!(grepper.run().is_ok()); | |
} | |
// Simulates a failed file read. | |
struct ErrorTestReader; | |
impl Reader for ErrorTestReader { | |
fn read(&mut self) -> io::Result<String> { | |
Err(io::Error::new(io::ErrorKind::Other, "oh no!")) | |
} | |
} | |
#[test] | |
fn test_run_errors_with_bad_file_read() { | |
let mut grepper = internal::Grepper { | |
reader: ErrorTestReader, | |
printer: DummyPrinter, | |
}; | |
assert!(grepper.run().is_err(), | |
"reader's error went unpropagated"); | |
} | |
// Keeps track of whether read() has been called on it, | |
// to make sure that Grepper's run() triggers the right side effects. | |
struct SpyReader { | |
pub read_called: bool | |
} | |
impl SpyReader { | |
fn new() -> SpyReader { | |
SpyReader { read_called: false } | |
} | |
} | |
impl Reader for SpyReader { | |
fn read(&mut self) -> io::Result<String> { | |
self.read_called = true; | |
Ok(String::from("ok")) | |
} | |
} | |
#[test] | |
fn test_run_calls_read() { | |
let mut grepper = internal::Grepper { | |
reader: SpyReader::new(), | |
printer: DummyPrinter, | |
}; | |
grepper.run().unwrap_or_else(|err_msg| | |
panic!("this test unexpectedly crashed: {}", err_msg) | |
); | |
assert!(grepper.reader.read_called, | |
"run() did not attempt to read in data"); | |
} | |
// Keeps track of whether print() has been called on it, | |
// to make sure that Grepper's run() triggers the right side effects. | |
struct SpyPrinter { | |
print_called: bool, | |
} | |
impl SpyPrinter { | |
fn new() -> SpyPrinter { | |
SpyPrinter { print_called: false } | |
} | |
} | |
impl Printer for SpyPrinter { | |
fn print(&mut self, _: String) -> () { | |
self.print_called = true; | |
} | |
} | |
#[test] | |
fn test_run_calls_print() { | |
let mut grepper = internal::Grepper { | |
printer: SpyPrinter::new(), | |
reader: OKTestReader, | |
}; | |
grepper.run().unwrap_or_else(|err_msg| | |
panic!("this test unexpectedly crashed: {}", err_msg) | |
); | |
assert!(grepper.printer.print_called, | |
"run() did not attempt to output text"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment