Logger mock for Rust unit tests

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

Traits in Rust are nicer than I imagined

Breaking news

Recently, I discovered that, besides serving as an interface, a trait in Rust can help create decoupled decorators. Two things really caught my eye this week.

One is the progress method used on an iterator by the indicatif crate. I include the ProgressIterator trait, and I can call progress() on an Iterator.

use indicatif::ProgressIterator;

for _ in (0..file_count).progress() {
    //...
}

The other: anyhow offers a context() method from its Context trait, to attach details to a Result.

use anyhow::Context;

func().context("error details")?;

Just by including the traits, their methods are available for some types. I imagined that there are implementations of those traits for some specific types, but I had no idea how they are done.

What I want to get to

It was clear that I needed to create a trait that has the method I want to implement, and to implement the trait for the type I want it to decorate. I chose to decorate iterators with a count_where method that accepts a predicate and counts the elements that meet the condition implemented in the predicate.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn count_where() {
        let event_number =
            vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].into_iter().count_where(|number| number % 2 == 0);
        assert_eq!(event_number, 5)
    }
}

Step by step

I created a CountWhere trait that works with types that implement the Iterator trait.

This trait has a method count_where that accepts a predicate P and returns a number of elements (usize).

The predicate accepts an argument with type reference to the type that the iterator’s elements are (&Self::Item) and returns a bool (to indicate whether the current element should be counted or not).

pub trait CountWhere: Iterator {
    fn count_where<P>(self, predicate: P) -> usize
    where
        P: Fn(&Self::Item) -> bool;
}

And I needed to provide the implementation for iterators, which is very simple: I just filter the iterator by the given predicate and count the elements.

impl<I> CountWhere for I
where
    I: Iterator,
{
    fn count_where<P>(self, predicate: P) -> usize
    where
        P: Fn(&Self::Item) -> bool,
    {
        self.filter(predicate).count()
    }
}

Next

While the idea is simple, I still feel there are more details to dig into. I need to know what I don’t know.

pub trait CountWhere: Iterator {
    fn count_where<P>(self, predicate: P) -> usize
    where
        P: Fn(&Self::Item) -> bool;
}

impl<I> CountWhere for I
where
    I: Iterator,
{
    fn count_where<P>(self, predicate: P) -> usize
    where
        P: Fn(&Self::Item) -> bool,
    {
        self.filter(predicate).count()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn count_where() {
        let event_number =
            vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].into_iter().count_where(|number| number % 2 == 0);
        assert_eq!(event_number, 5)
    }
}