Write tests using a mock client

The Cloud Client Libraries for Rust let you stub real client implementations and inject a mock for unit testing. Using a mock client lets you write controlled, reliable unit tests that don't make network calls or incur billed charges.

Before you begin

Before running the code in this document, follow Get started with Rust to install Rust and set up a Cargo project.

Dependencies

This document uses the mockall crate as its mocking framework and the Speech client from Speech-to-Text to illustrate the workflow, though the same pattern applies to other generated clients.

Add the client library and google-cloud-gax to your Cargo.toml file:

cargo add google-cloud-speech-v2 google-cloud-gax

Add mockall and tokio as development dependencies:

cargo add --dev mockall
cargo add --dev tokio --features macros,rt-multi-thread

Mock a client

To test your code without network calls, implement the service's stub trait on a mock struct and configure its expectations. Then construct a client with from_stub and pass it to your application function:

  1. Add use aliases to shorten crate names:

    use google_cloud_gax as gax;
    use google_cloud_speech_v2 as speech;

  2. Write an application function that accepts a reference to the Speech client, calls get_recognizer, and processes 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)
    }

  3. Define a mock struct that implements the speech::stub::Speech trait using mockall::mock!:

    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>>;
        }
    }

  4. In your test, create an instance of the mock. The mockall::mock! macro prepends Mock to the struct name to create MockSpeech:

    let mut mock = MockSpeech::new();

  5. Configure expectations on the mock to validate incoming request fields and return a simulated Recognizer response:

    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"),
            ))
        });

  6. Construct a Speech client from the mock using Speech::from_stub:

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

  7. Call the application function with the mocked client:

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

  8. Verify the returned value:

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

Simulate errors

To test error handling, configure the mock expectation to return an Err constructed with Error::service:

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))
});

Complete code

The following example combines both the success and error unit tests into a complete test module:

#[cfg(test)]
mod tests {
    use google_cloud_gax as gax;
    use google_cloud_speech_v2 as speech;
    type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

    // 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)
    }

    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>>;
        }
    }

    #[tokio::test]
    async fn basic_success() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        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 client, implemented by the mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function.
        let display_name = my_application_function(&client).await?;

        // Verify the final result of the RPC.
        assert_eq!(display_name, "test-display-name");

        Ok(())
    }

    #[tokio::test]
    async fn basic_fail() -> Result<()> {
        let mut mock = MockSpeech::new();
        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))
        });

        // Create a client, implemented by the mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function.
        let display_name = my_application_function(&client).await;

        // Verify the final result of the RPC.
        assert!(display_name.is_err(), "{display_name:?}");

        Ok(())
    }
}

Run cargo test to execute the unit tests:

cargo test