aws-smithy-mocks 0.2.2

Testing utilities for smithy-rs generated clients
Documentation
# 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, &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(_)));
}
```

<!-- anchor_start:footer -->
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.
<!-- anchor_end:footer -->