The Cloud Client Libraries for Rust provide a way to stub real client implementations so that you can inject a mock for testing. You can use mocks to write controlled, reliable unit tests that don't make network calls or incur billed charges.
In this document, you learn about the following topics:
- How to write testable interfaces using the Cloud Storage
Storageclient. - How to mock read operations.
- How to mock write operations.
- Why the design of the
Storageclient differs from other Google Cloud clients.
Dependencies
Add the Cloud Storage client library to your Cargo.toml file:
cargo add google-cloud-storage
To compile and run the unit tests in this document, add the following development dependencies:
cargo add --dev anyhow bytes mockall
cargo add --dev tokio --features macros,rt-multi-thread
Write testable interfaces
If you don't unit test your storage code, you can write interfaces directly in
terms of Storage, whose default stub type parameter is the production
implementation of the client:
To test your code with a mock client, write your interfaces in terms of a
generic type parameter T constrained by gcs::stub::Storage + 'static:
Mock read operations
To mock read_object requests, complete the following steps:
Write an application function that takes a generic
&Storage<T>client. For example, the following function downloads an object and counts how many newline characters it contains:Create a helper function that constructs a fake
ReadObjectResponsestream of bytes by passing a payload toReadObjectResponse::from_source. This method accepts the same payload types asStorage::write_object:Define a mock struct that implements
gcs::stub::Storage. This document uses themockallcrate, though you can use a different mocking framework in your tests:Write a unit test that sets expectations on the mock, constructs a client using
Storage::from_stub, and callscount_newlines:
Mock write operations
To mock write_object requests, complete the following steps:
Write an application function that uploads an object using a generic
&Storage<T>client:Using the
MockStoragestruct defined in the previous section, write a unit test that configures expectations on the mock, constructs a client usingStorage::from_stub, and callsupload:
Expectation details
Because the upload function calls send_unbuffered, set the expectation on
the corresponding expect_write_object_unbuffered method:
The mockall::mock! macro treats each generic instantiation as a distinct
method. Specify the concrete payload type, Payload<BytesSource>, in the
closure signature so that the compiler can match the method expectation:
Design rationale
The following sections explain why the Storage client uses a generic type
parameter for its stub rather than dynamic dispatch.
Other clients
Most clients, such as StorageControl, hold a reference-counted (Arc),
dyn-compatible implementation of their stub trait internally. They use
dynamic dispatch to forward requests from the client to the underlying stub,
which can be either the production implementation or a mock.
Because these clients use dynamic dispatch, the compiler doesn't need to resolve the concrete stub type at compile time, and the client structs don't need a generic type parameter for their stub.
The Storage client
In Rust, a dyn-compatible trait can't have generic methods or return unboxed
impl Future values:
- The
write_object_bufferedandwrite_object_unbufferedmethods are generic over the payload source type. - The
gcs::stub::Storagetrait methods returnimpl Futuredirectly instead of boxing the returned future.
Using dynamic dispatch for the Storage client would require boxing payloads
and futures on every call, adding heap allocations and dynamic dispatch
overhead on the data path. To avoid this overhead, the Storage client uses a
type parameter for a concrete implementation of the gcs::stub::Storage trait
instead.
Complete application code and test suite
The following code combines the application functions and unit tests into a complete example:
Run cargo test to execute test_count_lines and test_upload:
cargo test