After implementing my newtype in Better handling of Command output trying to write unit tests for 100% coverage was a non-starter. This, of course, was because internally it was instantiating std::process::Command::new itself. I set about looking for some kind of way to perform dependency injection.

I stumbled across a great blog post by Ecky Putrady on Structuring Rust Projects for Testability which talks about Hexagonal architecture, otherwise known as the “Ports and Adapters pattern”. The principles behind this pattern, along with the onion architecture amongst others, were combined by Robert C. Martin in 2012 to form the clean architecture. I am not yet sure if this is a Rust idiom, but figured it was worth taking a look.

Let us start with our ports (aka traits). We will need two. Firstly, an ‘in port’ called Program contains one method that allows us to execute returning a Result<Output, Error>. Both Output and Error are our own types (obmitted here for brievaty):

pub trait Program {
    fn execute(&mut self) -> Result<Output, Error>;
}

Now we have an ‘out port’ to describe how we collect the Output from the underlying implementation:

pub trait Command {
    fn output(&mut self) -> io::Result<std::process::Output>;
}

Notice we are using the standard library type here. I have done this for two reasons:

  1. For testing it is easy to construct
  2. The rabbit hole goes deeper… Output makes use of ExitStatus, which we would also need, so better stop soonerâ„¢

With our traits written, we can turn to the adapter. We only need the one for std::process::Command, which is straightforward:

impl Command for std::process::Command {
    fn output(&mut self) -> io::Result<std::process::Output> {
        self.output()
    }
}

Bringing this all together our domain, the ProgramImpl struct, which is generic over our Command trait:

pub struct ProgramImpl<T: Command> {
    command: T,
    expected_status_code: i32,
}

impl<T> Program for ProgramImpl<T>
where
    T: Command,
{
    fn execute(&mut self) -> Result<Output, Error> {
        let output = self.command.output()?;
        let status_code = output.status.code().ok_or(Error::NoStatusCode)?;
        let result = Output {
            status_code,
            stdout: output.stdout,
            stderr: output.stderr,
        };

        if status_code != self.expected_status_code {
            return Err(result.into());
        }
        Ok(result)
    }
}

To bring this all together visually we have the following:

C o n s u m e r U s e s ` ( ` ( C a P a o k r k m a o a m g a a r p n d a o d a m r ` p ` t t ) i e t m r r p ) a l i t I I m m p p l l e e m m e e n n t t s s ` ( ` ( P a C a r k o k o a m a g m r d a p a o U n o m m s d r I a e ` t m i s ) p n t l ) r ` a i t

The result is we can:

  • construct any standard library Command
  • ensure that the command exit code is checked and stdout and stderr are available to the consumer
  • test all the code in the domain (ProgramImpl)

My project that uses this implementation is still a work in progress, but here is a working snippet for now:

let mut defaults = Command::new("defaults");
defaults.args(["read", "com.apple.dock", "autohide"]);
let output = ProgramImpl::new(defaults, 0).execute()?;