Context
I’m writing a CLI app in Rust with clap. Some commands result in one or multiple messages being written to stdout and/or stderr. These messages are an actual part of the commands’ result, not just “some logs”. So I need to verify their integrity.
I wrote integration tests with assert_cmd and predicates, but I wanted unit tests for each of my commands. Some commands interact with external tools that need to be mocked for testing. Even printing messages is an external interaction with stdout and stderr.
Directly capturing stdout and stderr seems not to be an easy approach. I found the capture_stdio crate, but it needs the nightly toolchain.
There might be other better approaches, but the learning process is a good enough reason for me to go on the path of implementing a solution.
Inject everything
For a simple CLI app, I consider println!/eprintln! enough. And I wanted to stick to them. I went searching for another approach just to have the possibility to fully unit test my code.
One of the first ideas was obvious: dependency injection. Inject a logger into the task (a struct with a function) that is attached to the command. Which is usually the way to go. But why go easy when going hard will push me into learning more? It seemed a bit of overkill to go from println to injecting dependencies.
And I reached out to loggers. The log facade lets you change between multiple logger implementations. And I wanted to write a simple custom logger (again, mostly for learning). Although it implies a shared global instance of the logger and, further down in my implementation, a singleton, I let the inconvenience aside and followed my goal of unit testing the tasks my CLI app runs.
What I need
I knew I needed a production logger that writes messages on stdout/stderr. I chose to write on stdout the info-level messages and on stderr all of the other ones.
use log::{Level, Metadata, Record}; struct Logger; impl log::Log for Logger { fn enabled(&self, metadata: &Metadata<'_>) -> bool { metadata.level() <= Level::Info } fn log(&self, record: &Record<'_>) { if self.enabled(record.metadata()) { match record.level() { Level::Info => println!("{}", record.args()), _ => eprintln!("{}", record.args()), } } } fn flush(&self) {} }
And a test logger that writes the messages somewhere I can get them and verify they are correct. Continue reading Logger mock for Rust unit tests