# aws-smithy-mocks
A flexible mocking framework for testing clients generated by smithy-rs, including all packages of the AWS SDK for Rust.
This crate provides a simple yet powerful way to mock SDK client responses for testing purposes.
It uses interceptors to return stub responses, allowing you to test both happy-path and error scenarios
without mocking the entire client or using traits.
## Key Features
- **Simple API**: Create mock rules with a fluent API using the [`mock!`] macro
- **Flexible Response Types**: Return modeled outputs, errors, or raw HTTP responses
- **Request Matching**: Match requests based on their properties
- **Response Sequencing**: Define sequences of responses for testing retry behavior
- **Rule Modes**: Control how rules are matched and applied
## Prerequisites
<div class="warning">
You must enable the `test-util` feature of the service client crate in order to use the `mock_client` macro. This should
usually only be enabled as part of your `dev-dependencies` (see the example `Cargo.toml` below).
</div>
If the feature is not enabled a compilation error similar to the following will occur:
```txt
no method named with_test_defaults found for struct <service-client-crate>::config::Builder in the current scope
method not found in Builder
```
Example `Cargo.toml` using the `aws-sdk-s3` crate as the service client crate under test:
```toml
[dependencies]
aws-sdk-s3 = "1"
[dev-dependencies]
aws-smithy-mocks = "0.2"
aws-sdk-s3 = { version = "1", features = ["test-util"] }
```
## Basic Usage
```rust,ignore
use aws_sdk_s3::operation::get_object::GetObjectOutput;
use aws_sdk_s3::Client;
use aws_smithy_types::byte_stream::ByteStream;
use aws_smithy_mocks::{mock, mock_client};
#[tokio::test]
async fn test_s3_get_object() {
// Create a rule that returns a successful response
let get_object_rule = mock!(Client::get_object)
.match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
.then_output(|| GetObjectOutput::builder()
.body(ByteStream::from_static(b"test-content"))
.build()
);
// Create a mocked client with the rule
let s3 = mock_client!(aws_sdk_s3, [&get_object_rule]);
// Use the client as you would normally
let result = s3.get_object()
.bucket("test-bucket")
.key("test-key")
.send()
.await
.expect("success response");
// Verify the response
let data = result.body.collect().await.expect("successful read").to_vec();
assert_eq!(data, b"test-content");
// Verify the rule was used
assert_eq!(get_object_rule.num_calls(), 1);
}
```
## Creating Rules
Rules are created using the [`mock!`] macro, which takes a client operation as an argument:
```rust,ignore
let rule = mock!(Client::get_object)
// Optional: Add a matcher to filter requests
.match_requests(|req| req.bucket() == Some("test-bucket"))
// Add a response
.then_output(|| GetObjectOutput::builder().build());
```
### Response Types
You can return different types of responses:
```rust,ignore
// Return a modeled output
let success_rule = mock!(Client::get_object)
.then_output(|| GetObjectOutput::builder().build());
// Return a modeled error
let error_rule = mock!(Client::get_object)
.then_error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
// Return an HTTP response
let http_rule = mock!(Client::get_object)
.then_http_response(|| HttpResponse::new(
StatusCode::try_from(503).unwrap(),
SdkBody::from("service unavailable")
));
// Return a computed output based on the input
let compute_rule = mock!(Client::get_object)
.then_compute_output(|req| {
let key = req.key().unwrap_or("unknown");
GetObjectOutput::builder()
.body(ByteStream::from_static(format!("content for {}", key).as_bytes()))
.build()
});
// Return any response type (output, error, or HTTP) based on the input
let conditional_rule = mock!(Client::get_object)
.then_compute_response(|req| {
use aws_smithy_mocks::MockResponse;
if req.key() == Some("error-key") {
MockResponse::Error(GetObjectError::NoSuchKey(NoSuchKey::builder().build()))
} else {
MockResponse::Output(GetObjectOutput::builder()
.body(ByteStream::from_static(b"success"))
.build())
}
});
```
### Response Sequences
For testing retry behavior or complex scenarios, you can define sequences of responses using the sequence builder API:
```rust,ignore
let retry_rule = mock!(Client::get_object)
.sequence()
.http_status(503, None) // First call returns 503
.http_status(503, None) // Second call returns 503
.output(|| GetObjectOutput::builder().build()) // Third call succeeds
.build();
// With repetition using `times()`
let retry_rule = mock!(Client::get_object)
.sequence()
.http_status(503, None)
.times(2) // First two calls return 503
.output(|| GetObjectOutput::builder().build()) // Third call succeeds
.build();
// Repeat a response indefinitely
let infinite_rule = mock!(Client::get_object)
.sequence()
.error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build()))
.output(|| GetObjectOutput::builder().build()) // Second call succeeds
.repeatedly() // All subsequent calls succeed
.build();
```
The `times(n)` method repeats the last added response `n` times, while `repeatedly()` causes the last response to
repeat indefinitely, making the rule never exhaust.
The sequence builder API provides a fluent interface for defining sequences of responses.
After providing all responses in the sequence, the rule is considered exhausted.
## Creating Mocked Clients
Use the [`mock_client!`] macro to create a client with your rules:
```rust,ignore
// Create a client with a single rule
let client = mock_client!(aws_sdk_s3, [&rule]);
// Create a client with multiple rules and a specific rule mode
let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, [&rule1, &rule2]);
// Create a client with additional configuration
let client = mock_client!(
aws_sdk_s3,
RuleMode::Sequential,
[&rule],
|config| config.force_path_style(true)
);
```
## Rule Modes
The [`RuleMode`] enum controls how rules are matched and applied:
Given a simple (non-sequenced) based rule (e.g. `.then_output()`, `.then_error()`, or `.then_http_response()`):
- `RuleMode::Sequential`: The rule is used once and then the next rule is used.
- `RuleMode::MatchAny`: Rule is used repeatedly as many times as it is matched.
In other words, simple rules behave as single use rules in `Sequential` mode and as infinite sequences in `MatchAny` mode.
Given a sequenced rule (e.g. via `.sequence()`):
- `RuleMode::Sequential`: Rules are tried in order. When a rule is exhausted, the next rule is used.
- `RuleMode::MatchAny`: The first (non-exhausted) matching rule is used, regardless of order.
```rust,ignore
let interceptor = MockResponseInterceptor::new()
.rule_mode(RuleMode::Sequential)
.with_rule(&rule1)
.with_rule(&rule2);
```
## Testing Retry Behavior
The mocking framework supports testing retry behavior by allowing you to define sequences of responses:
```rust,ignore
#[tokio::test]
async fn test_retry() {
// Create a rule that returns errors for the first two attempts, then succeeds
let rule = mock!(Client::get_object)
.sequence()
.http_status(503, None)
.times(2) // Service unavailable for first two calls
.output(|| GetObjectOutput::builder().build()) // Success on third call
.build();
// Create a client with retry enabled
let client = mock_client!(aws_sdk_s3, [&rule]);
// The operation should succeed after retries
let result = client.get_object()
.bucket("test-bucket")
.key("test-key")
.send()
.await;
assert!(result.is_ok());
assert_eq!(rule.num_calls(), 3); // Called 3 times (2 failures + 1 success)
}
```
### Testing Different Responses Based on Request Parameters
```rust,ignore
#[tokio::test]
async fn test_different_responses() {
// Create rules for different request parameters
let exists_rule = mock!(Client::get_object)
.match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("exists"))
.sequence()
.output(|| GetObjectOutput::builder()
.body(ByteStream::from_static(b"found"))
.build())
.build();
let not_exists_rule = mock!(Client::get_object)
.match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("not-exists"))
.sequence()
.error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build()))
.build();
// Create a mocked client with the rules in MatchAny mode
let s3 = mock_client!(aws_sdk_s3, RuleMode::MatchAny, [&exists_rule, ¬_exists_rule]);
// Test the "exists" case
let result1 = s3
.get_object()
.bucket("test-bucket")
.key("exists")
.send()
.await
.expect("object exists");
let data = result1.body.collect().await.expect("successful read").to_vec();
assert_eq!(data, b"found");
// Test the "not-exists" case
let result2 = s3
.get_object()
.bucket("test-bucket")
.key("not-exists")
.send()
.await;
assert!(result2.is_err());
assert!(matches!(result2.unwrap_err().into_service_error(),
GetObjectError::NoSuchKey(_)));
}
```
This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/smithy-lang/smithy-rs) code generator.