Write tests using a mock client

Learn how to stub real client implementations to inject a mock for unit testing. Using a mock client with the Google Cloud Client Libraries for Rust lets you write controlled, reliable unit tests that don't make network calls or incur billing charges.

Dependencies

There are several mocking frameworks available for Rust. This guide uses mockall. Add it as a development dependency:

cargo add --dev mockall

Additionally, this guide uses the Speech client to make examples easier to follow (but these concepts apply to all clients).

Add the required dependencies to your Cargo.toml file:

cargo add google-cloud-speech-v2 google-cloud-lro

Mock a client

To test your code with a mock client, you define a mock struct, configure its expected behavior for your test scenario, and then inject that mock into your application logic. The following example demonstrates this workflow.

First, add use statements to simplify the code:

use google_cloud_gax as gax;
use google_cloud_speech_v2 as speech;

Assume the application has a function that uses the Speech client to call GetRecognizer, setting the name field of the request, and process the server response.

// An example application function.
//
// It makes an RPC, setting some field. In this case, it is the `GetRecognizer`
// RPC, setting the name field.
//
// It processes the response from the server. In this case, it extracts the
// display name of the recognizer.
async fn my_application_function(client: &speech::client::Speech) -> gax::Result<String> {
    client
        .get_recognizer()
        .set_name("invalid-test-recognizer")
        .send()
        .await
        .map(|r| r.display_name)
}

You can test how the application handles different responses from the service.

Next, define the mock struct. This struct implements the speech::stub::Speech trait.

mockall::mock! {
    #[derive(Debug)]
    Speech {}
    impl speech::stub::Speech for Speech {
        async fn get_recognizer(&self, req: speech::model::GetRecognizerRequest, _options: gax::options::RequestOptions) -> gax::Result<gax::response::Response<speech::model::Recognizer>>;
    }
}

Create an instance of the mock. Note that the mockall::mock! macro prepends a Mock prefix to the name of the previously defined struct.

let mut mock = MockSpeech::new();

Set expectations on the mock. For example, expect the code to call GetRecognizer with a particular name and simulate a successful response from the service.

mock.expect_get_recognizer()
    .withf(move |r, _|
        // Optionally, verify fields in the request.
        r.name == "invalid-test-recognizer")
    .return_once(|_, _| {
        Ok(gax::response::Response::from(
            speech::model::Recognizer::new().set_display_name("test-display-name"),
        ))
    });

Create a Speech client using the mock:

let client = speech::client::Speech::from_stub(mock);

Call the function:

let display_name = my_application_function(&client).await?;

Verify the results:

assert_eq!(display_name, "test-display-name");

Simulate errors

Simulating errors is similar to simulating successes. To simulate an error, modify the result returned by the mock.

mock.expect_get_recognizer().return_once(|_, _| {
    // This time, return an error.
    use gax::error::Error;
    use gax::error::rpc::{Code, Status};
    let status = Status::default()
        .set_code(Code::NotFound)
        .set_message("Resource not found");
    Err(Error::service(status))
});

A client built with from_stub() does not have an internal retry loop; it returns all errors from the stub directly to the application.

Next steps

To view the complete code from this guide, see the source file in the google-cloud-rust repository on GitHub.