Mocking structures/methods in Rust

I have a struct named Device:

pub struct Device
{
  id: u32,
  name: String,
  enable : bool,
  peripheral: DevicePeripheral, // Another structure
}

impl Device
{
  pub fn new(id: u32, name: String, enable: bool, peripheral: DevicePeripheral) -> Self
  {
     Self {id, name, enable, peripheral}
  }

  // Turn on the device
  pub fn turn_on(&self)
 {
    self.enable = self.peripheral.power_on();
 }
 
  // check whether device is turned on
  pub fn is_enabled(&self) -> bool
  {
       return self.enable;
  }

}

The above structure and it's implementation block are defined in another crate while the unit test that I am writing is in different crate.

mod test
{
  #[test]
  fn test_check_turn_on()
  {
    let p = Peripheral::new();

    // Instantiate the device 
    let dev = Device::new(0x1, "timer".to_string(), 0, p);
     dev.turn_on();
    let ret = dev.is_enabled();
    assert_eq!(ret, true);
  //
  }
}

I need to instantiate the device struct in my test and test the turn_on() functionality. I don't have access to peripheral module since its dependent on actual hardware. So, self.enable = self.peripheral.turn_on() should be replaced by self.enable = 1 in mocked function. How can I do that?

I came across mockall crate which says to define a trait that implements turn_on() function.
The same trait should also be implemented by the Device struct and MockedDevice struct .
However, it feels unnecessary to modify the production code to define another trait(for just one method) just to support testing. Is there any other way ?
Specifically, I was looking to create a fake implementation of Device struct without the peripheral member and then implementing the turn_on() method that sets enable to 1.

Note: The code is redacted version of original code(with names modified) due to confidential purposes. Expect some minor typo in the code.

Fundamentally, given your current definitions, saying that some value is a Device means that it meets the properties of the Device type, which include that it owns a DevicePeripheral. You cannot create a Device that does not own a DevicePeripheral without changing the definition of Device.

Creating a trait to allow for a mock implementation is one way to solve this problem. I would recommend that before you do that, you should first refactor in a “sans-io” direction: as much as possible of the code of Device should exist in a form that doesn't use DevicePeripheral (perhaps in an impl for a separate struct), and as much as possible of the code that currently uses Device should exist in a form that doesn’t use Device. You may then still need a trait, but you will need to actually use it less, and you are less likely to have an uncaught bug resulting from the mock not behaving like the real thing.

You should also look for opportunities to create a more test-compatible form that are not solely for testing. Find some way in which it makes sense to have a Device without an actual DevicePeripheral, if you can.

4 Likes

Instead of mocking Device with a trait, consider mocking the hardware API instead. That will let you both explicitly define what parts of the hardware API you rely on and also lets you verify the code’s response when oddball things happen by using different mocks in different tests (like the peripheral failing to turn on).

pub struct Device<T=DevicePeripheral>
{
  id: u32,
  name: String,
  enable : bool,
  peripheral: T,
}

/// The subset of the hardware API that `Device` relies on
pub trait Peripheral {
    fn power_on(&mut self)->bool;
}

pub struct MockPeripheral;
impl Peripheral for MockPeripheral { … }

impl<T:Peripheral> Device<T>
{
  // …
}
1 Like

It's actually quite easy to mock Device as written, without refactoring it into a trait. You can do it like this:

pub struct Device
{
    ...
}
#[mockall::automock]
impl Device {
    ...
}

But if you do that, what will you actually be testing? Your test_check_turn_on function looks like it's designed to test the is_enabled method. But if you have a MockDevice, then is_enabled will be a mock function too. So the test would be pointless. In general, a mock object is only useful for testing the behavior of upstack code. That is, consumers of the regular object.

Also, I strongly recommend that you move the test code into the same crate as Device itself. Otherwise, you'll have to ship the MockDevice struct in your production crate. If you do move the two into the same crate, then you can do #[cfg_attr(test, mockall::automock)]. That way, Mockall will only need to be a dev-dependency, not a full dependency.