Mocking object's internal dependencies

Coming from Spring/Java world, I struggle to find a nice way to mock object's dependencies in Rust without adding too much test-related stuff to the API.

So, let's say, I have this code:

struct MyApiClient {
    api_token: String,
    driver: external_api::ExternalApiDriver
}

impl MyApiClient {
    fn new(api_token: &str) -> Self {
        let driver = external_api::ExternalApiDriver::new();

        Self { 
            api_token: api_token.to_owned(),
            driver: driver 
        }
    }

    fn call_external_api(&self) -> Result<String, Error> {
        driver.call_api(&token, "SOME_API_METHOD")
    }
}

During testing, I want to ensure that when calling the MyApiClient.call_external_api() method, the ExternalApiDriver.call_api() was called exactly once, with correct arguments.

Creating a mock for the external_api::ExternalApiDriver struct is easy, but I can't find a slick way to replace its default implementation within MyApiClient.

The external_api::ExternalApiDriver object is not used anywhere else in the program, so providing it through the new() constructor seems unnecessarily verbose.

Another option I can think if is creating a #[cfg(test)]-annotated setter method for the driver field, but to me it also doesn't feel like a clean solution, especially if there will be multiple dependencies.

In Java, stuff like this was handled by mocking libraries using dependency injection magic.

I would like to hear, what is the conventional way to hook on object's side effects in Rust, and, if there is none, what would be an appropriate solution to the problem.

Providing it through the new() constructor — or at least some constructor, not necessarily the usual one — is the reliable, boring, safe, non-magic way to do dependency injection. That is what you should do.

2 Likes

Before you go that way you need to ask yourself first why do you want to do that.

In practice precisely the thing that makes it hard to mock things also makes it unnecessary 99% of time. With Java you often want to ensure that certain method A called from the outside would call some other method X in a different layer — because someone may decide to override X and you want to know… well, in Rust you couldn't override anything and that means that you don't care.

In cases where you do care (like in your example with ExternalApiDriver ) you would be getting said driver in your new method instead of creating it from the thin air — in these cases passing different driver for testing purposes is trivial.

3 Likes

If it is acceptable for your use case to construct the value with a default instance and then replace with the mock afterwards, you can define a method like:

fn with_driver(mut self, driver: ExternalApiDriver) -> Self {
  self.driver = driver;
  self
}

And use like so:

let client = ApiClient::new().with_driver(mock_driver);

This is similar to the field setter idea, but unlike a setter this can only be used to create new instances, not modify existing ones.

If passing the mock via new instead, you can use the Default trait to minimize boilerplate:

#[derive(Default)]
struct ApiClientConfig {
  driver: Option<ExternalApiDriver>,
}

fn new(config: ApiClientConfig) -> Self {
  let driver = config.driver.unwrap_or_default();
  ...
}

let client = ApiClient::new(ApiClientConfig {
  driver: Some(custom_driver),
  ..Default::default() // Use defaults for everything else.
})
1 Like