Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

How to write tests using a client

The Google Cloud Client Libraries for Rust provide a way to stub out the real client implementations, so you can inject a mock for testing.

Applications can use mocks to write controlled, reliable unit tests that do not involve network calls, and do not incur billing.

This guide shows how.

Dependencies

There are several mocking frameworks in Rust. This guide uses mockall, which seems to be the most popular.

cargo add --dev mockall

This guide will use a Speech client. Note that the same ideas in this guide apply to all of the clients, not just the Speech client.

Add the dependency to your Cargo.toml file:

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

Mocking a client

First, some use declarations 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 make an RPC, and process the response from the server.

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

To test how the code handles different responses from the service.

First, define the mock class. This class 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>>;
        }
    }

Next, 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();

Next, set expectations on the mock. Expect the code to call GetRecognizer with a particular name.

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

Now, create a Speech client with the mock.

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

Finally, call the function…

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

… and verify the results.

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

Simulating errors

Simulating errors is no different than simulating successes. You just need to 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))
        });

Note that a client built from_stub() does not have an internal retry loop. It returns all errors from the stub directly to the application.


Full program

Putting all this code together into a full program looks as follows:

#[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(())
    }
}