Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Code Reviews

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Writing a testable console program

+4
−0

I have a class Foo that prints something to stdout and I want to be able to write tests for it.

So I created a trait to abstract println!, and gave it a prod implementation and a test implementation.

The test implementation simply writes the strings to a vector, so that tests can make assertions on the contents of the vector.

The problem is that since the MockIO modifies itself, I'm forced to write the method's signature as fn println(&mut self, s: &str) instead of fn println(&self, s: &str). Which in turns forces me to sprinkle muts all over the code.

// main.rs
mod foo;
mod io;

fn main() {
    let mut io_interface = io::StdIO{};
    foo::Foo::print_hello(&mut io_interface);
}

// foo.rs
use crate::io::{self, IO};

pub struct Foo {
}

impl Foo {
    pub fn print_hello<I>(io_interface: &mut I) where I: IO {
        io_interface.println("Hello, World!");
    }
}

#[cfg(test)]
#[test]
fn print_hello_world() {
    let mut mock_io = io::MockIO::new();

    Foo::print_hello(&mut mock_io);

    assert_eq!(mock_io.outputs[0], "Hello, World!");
}
// io.rs
pub struct StdIO;

pub trait IO {
    fn println(&mut self, s: &str);
}

impl IO for StdIO {
    fn println(&mut self, s: &str) {
        println!("{}", s);
    }
}


#[cfg(test)]
pub struct MockIO {
    pub outputs: Vec<String>,
}

#[cfg(test)]
impl IO for MockIO {
    fn println(&mut self, s: &str) {
        self.outputs.push(s.to_string());
    }
}

#[cfg(test)]
impl MockIO {
    pub fn new() -> Self {
        MockIO { outputs:vec![] }
    }
}

So my questions are:

  • is there a way to write MockIO so that I don't have to change the signature of IO::println() for the sake of the test class?
  • is there a better way to solve this problem altogether?

Feel free to point out any non-idiomatic code as well. I've done a fair bit of software development but am new to Rust.

History
Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

1 comment thread

Any particular reason you're writing your own IO trait? (2 comments)

2 answers

+4
−0

is there a way to write MockIO so that I don't have to change the signature of IO::println() for the sake of the test class?

Yes, implementing mock objects is one of the recommended uses for RefCell<T>, according to the Rust book.

By placing the vector in a RefCell you gain the ability to modify it (carefully!) via a non-mut reference.

struct MockIO {
    outputs: RefCell<Vec<String>>,
}

impl IO for MockIO {
    fn println(&self, s: &str) {
        self.outputs.borrow_mut().push(s.to_string());
    }
}

I've also removed the pubs from the test code in this example: generally test code should live in the same module (or a sub-module) as the code it is testing, and does not need to be exposed publicly. If the struct does need to be exposed for some reason, the RefCell certainly shouldn't be pub, because it would be impossible to enforce the requirement that there is never more than one borrow_mut() active at once (otherwise the code will panic at runtime).

is there a better way to solve this problem altogether?

Personally I think this approach is rather too complicated. I presume that it is not the act of printing that you want to test, but just that your class generates the correct string contents.

I would test this by simply splitting the printing code from the string-generating code, then only test the generating code while assuming that the basic Rust functionality of printing to stdout works fine.

E.g.

impl Foo {
    fn make_hello() -> String {
        String::from("Hello, World!")
    }
    pub fn print_hello() {
        let line = make_hello();
        println!("{line}");
    }
}

Now you can write all the tests for make_hello() that you require by examining the returned string, without needing any abstracted IO traits.

Note the different visibility of the two methods: print_hello() is pub because it is intended to be called by client code, whereas make_hello() is not pub because it is only intended to be called in unit tests within the same module. You could of course choose to make make_hello() public if there was a potential need for client code to generate an output string without immediately printing it.

If you really do want to test printing rather than just string generation, or you find the allocation of String objects unacceptable for any reason, then you should use the std::io::Write trait exposed in the standard library (as recommended by Moshi in the comments) rather than defining your own.

Organising tests

Instead of adding a #[cfg(test)] to each individual chunk of test code, it is more idiomatic to put all of the test code in a tests submodule at the end of the module being tested. This makes the tests easy to find, and keeps them private while allowing them to access all of the code in the parent module due to Rust's module visibility rules.

#[cfg(test)]
mod tests {
    use super::*; // import everything from parent module

    // Define mock objects, other test-specific code here
    struct MockIO {
        outputs: RefCell<Vec<String>>,
    }

    impl IO for MockIO {
        // etc
    }

    // Actual test functions here
    #[test]
    fn make_hello_world() {
        // etc
    }
}
History
Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

+2
−0

Not using Rust myself I'll add to @InfiniteDissent's answer on:

is there a better way to solve this problem altogether?

Yes, there is, it's called a golden master test.

Legacy code retreat taught me a golden master testing technique for cases just like that.

  1. You create a set of inputs for your application.
  2. You capture a set of outputs for these inputs, creating working pairs: i1 -> o1, i2 -> o2, ... in -> on. That is your golden master.
  3. For each change you should run all inputs and COMPARE the outputs with the golden master ones.

For console-heavy apps I'd usually do the following:

  1. make your CLI app accept an array of inputs (or a file with inputs).
  2. have the app source inputs from said array/file.
  3. capture the output by redirecting it to a file.
  4. Assuming that we have 40 sets of inputs: for i in $(seq 40); do ./app input$i > output$i; done; diffOutputsWithGoldenMaster.sh
  5. For diffing the outputs, sometimes just diff is enough (I always tried to make it so). Sometimes, you want something special, usually to highlight clearer the parts broken.

In short: I'd move the testing outside of the Rust app. I had success with this technique also with SQL, where I wanted to make sure all reports worked before and after we completely overhauled the engine for their making, running and exporting. We had two Jenkins jobs, one for query generation (in your case, message creation) and one for actual results of the reports being the same (the console log of your app run) as the golden master.

AFAIR, the name is from music recording, where the original, often golden CD, was "the master record", a collectors item. And the source of mass-production of CD albums.

History
Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »