Expand description
§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
§Basic Usage
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:
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:
// 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")
));
§Response Sequences
For testing retry behavior or complex scenarios, you can define sequences of responses using the sequence builder API:
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:
// 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.
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:
#[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
#[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 and the smithy-rs code generator.
Macros§
- mock
mock!
macro that produces aRuleBuilder
from a client invocation- mock_
client mock_client!
macro produces a Client configured with a number of Rules and appropriate test default configuration.
Structs§
- Mock
Response Interceptor - Interceptor which produces mock responses based on a list of rules
- Rule
- A rule for matching requests and providing mock responses.
- Rule
Builder - A builder for creating rules.
Enums§
- Rule
Mode - RuleMode describes how rules will be interpreted.
Functions§
- create_
mock_ http_ client - Create a mock HTTP client that works with the interceptor using existing utilities