yaymukund’s weblog ^_^

How to spy on your Rust code

In the following code, how can you test that Player is making the correct API calls?

struct Player<'a> {
  api: &'a Api,
}

impl<'a> Player<'a> {
  pub fn new(api: &'a Api) -> Player<'a> {
    Player { api }
  }

  pub fn play(&self, song: &str) {
    self.api.sing(song);
  }

  pub fn stop(&self) {
    self.api.shush();
  }
}

The answer? Make it generic!

struct Player<'a, T> {
  api: &'a T,
}

impl<'a, T> Player<'a, T>
where
  T: PlayerApi,
{
  pub fn new(api: &'a T) -> Player<'a, T> {
    Player { api }
  }

  // ...
}

trait PlayerApi {
  // Default trait implementation uses `Api`
  fn sing(&self, url: &str);
  fn shush(&self);
}

impl PlayerApi for Api {
  fn sing(&self, url: &str) {
    Api::sing(self, url)
  }

  fn shush(&self) {
    Api::shush(self)
  }
}

Then you can easily spy on it:

#[cfg(test)]
module Test {
  struct ApiSpy {
    pub invocations: Vec<String>,
    api: Api,
  }

  impl ApiSpy {
    pub fn new() -> ApiSpy {
      ApiSpy { api: Api::New() }
    }
  }

  impl PlayerApi for ApiSpy {
    fn sing(&self, url: &str) {
      self.invocations.push('play');
      self.api.sing(url)
    }
  }

  #[test]
  fn test_play() {
    let api = ApiMock::new();
    let player = Player::new(&api);
    player.play("my_url");
    assert_eq!(api.invocations[0], "play");
  }
}

That's it!

How can I assert that I passed the correct arguments?

You can store the arguments in the ApiSpy. For example, here's how I mocked the mpv api and used it in a test.

I don't want to define a trait for my API just for tests!

Defining a trait for your external APIs is good for reasons beyond testing. But if you still still don't want to make your API generic, then you could use conditional compilation instead..

What if I don't want to execute API code in tests?

If you want to mock instead of spy, you have a couple options: