Crates.io | aws-smithy-mocks |
lib.rs | aws-smithy-mocks |
version | 0.1.1 |
created_at | 2025-05-06 11:07:58.022485+00 |
updated_at | 2025-05-19 21:55:36.011858+00 |
description | Testing utilities for smithy-rs generated clients |
homepage | |
repository | https://github.com/smithy-lang/smithy-rs |
max_upload_size | |
id | 1662263 |
size | 76,150 |
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.
mock!
] macrouse 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);
}
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());
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")
));
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.
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)
);
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);
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)
}
#[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.