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 a mock can be injected 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.

We declare the dependency in our Cargo.toml. Yours will be similar, but without the custom path.

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;

Let's assume our 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)
    }

We want to test how our code handles different responses from the service.

First we will 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, we create an instance of the mock. Note that the mockall::mock! macro prepends a Mock prefix to the name of our struct from above.

        let mut mock = MockSpeech::new();

Next we will set expectations on the mock. We expect GetRecognizer to be called, with a particular name.

If that happens, we will 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 we are ready to create a Speech client with our mock.

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

Finally, we are ready to call our 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. We just need to modify the result returned by our 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());

        Ok(())
    }
}