Crate aws_smithy_mocks

Source
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, &not_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 a RuleBuilder from a client invocation
mock_client
mock_client! macro produces a Client configured with a number of Rules and appropriate test default configuration.

Structs§

MockResponseInterceptor
Interceptor which produces mock responses based on a list of rules
Rule
A rule for matching requests and providing mock responses.
RuleBuilder
A builder for creating rules.

Enums§

RuleMode
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