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