Skip to content

Instantly share code, notes, and snippets.

@spinnylights
Last active December 11, 2020 11:56
Show Gist options
  • Save spinnylights/02308de189f7330da595fcdff394426c to your computer and use it in GitHub Desktop.
Save spinnylights/02308de189f7330da595fcdff394426c to your computer and use it in GitHub Desktop.
Rust book "minigrep" project: test doubles + spies
// 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