Table of Contents

Class FakeDynamoDb

Namespace
DynaSharp.Testing
Assembly
DynaSharp.Testing.dll

An in-memory DynamoDB that speaks the wire protocol behind an HttpMessageHandler, for functional tests: a test sets up tables and items, runs the code under test through a real client, and reads the state back. No network, no container, no queued answers.

public sealed class FakeDynamoDb
Inheritance
FakeDynamoDb
Inherited Members

Examples

var fake = new FakeDynamoDb();
fake.CreateTable<Customer>("Shop");
using var client = fake.CreateClient();

await client.Table<Customer>("Shop").Put(customer).SendAsync(ct); // the code under test

var stored = fake.Get<Customer>("Shop", StubResponse.KeyOf(Customer.Keys.Of("alice")));

Remarks

Clients. CreateClient(Func<DynamoDbClientOptions, DynamoDbClientOptions>?) gives a client over the fake, CreateStreamsClient(Func<DynamoDbClientOptions, DynamoDbClientOptions>?) one of DynamoDB Streams, and StubClient.Create(fake.CreateHandler()) is the same: CreateHandler() returns a StubHandler whose responder is Answer(RecordedRequest), so Requests records what the client sent, and a response queued with Enqueue(StubResponse) answers the next request instead of the fake, which never sees it: a queued throttle changes no state, as a throttle of DynamoDB does not. Every handler of one fake shares its state. A test of a write whose response is lost wraps the fake in a responder of its own that answers, and then returns an error in place of the answer.

What it answers. CreateTable, DescribeTable, UpdateTable, DeleteTable, ListTables, DescribeTimeToLive, UpdateTimeToLive, DescribeLimits, GetItem, PutItem, UpdateItem, DeleteItem, Query, Scan, BatchGetItem, BatchWriteItem, TransactGetItems and TransactWriteItems, with condition, update, key condition, filter and projection expressions, ReturnValues, ReturnConsumedCapacity and a transaction token that holds for 10 minutes on the clock, and of DynamoDB Streams ListStreams, DescribeStream, GetShardIterator and GetRecords: a table with a stream records every change of an item in a shard, in the order of the writes, the records of a request together and none of a request that failed. A table and its indexes are ACTIVE in the answer that creates them, an index holds the items of its table at once and follows every write at once, and every read sees the last write. Keys are checked against the schema with the messages of DynamoDB, as are the type of a key of an index, empty keys, the sizes of keys and the item limit of 400 KB, and numbers keep their 38 digits and come back in canonical form, 1.50 as 1.5.

What it does not do. An operation it does not answer, such as PartiQL, is HTTP 400 with UnknownOperationException. A member it does not implement inside an operation it answers, such as tags or a legacy parameter, is HTTP 400 with the code UnsupportedByFakeException, which names the member: the fake never ignores a member. Nothing throttles, nothing is eventually consistent, no transaction conflicts, a batch leaves nothing unprocessed, a stream has one shard until SplitStreamShards(string, int, TimeSpan) gives it children, and nothing expires by itself.

Time. The clock given to the constructor dates the tables and the stream records, expires shard iterators after 15 minutes and trims records after 24 hours, and ExpireItems() deletes what has expired by it. One fake per test, made with new: it holds no static state and nothing to dispose. It is thread-safe, and each request runs under one lock from start to end.

The direct API. CreateTable(CreateTableRequest), Put(string, Item), Get(string, Item), Delete(string, Item), Items(string) and the rest set up and read state without a client, and go through the same checks as a request: a key that does not match the schema throws the ValidationException a client would get.

Constructors

FakeDynamoDb(TimeProvider?)

Initializes a new instance of the FakeDynamoDb class, without tables.

public FakeDynamoDb(TimeProvider? timeProvider = null)

Parameters

timeProvider TimeProvider

The clock of creation times, of time to live, of the 10 minutes a client request token of a transaction holds and of the streams, System if null.

Properties

TableNames

Gets the names of the tables, in ordinal order, as ListTables gives them.

public IReadOnlyList<string> TableNames { get; }

Property Value

IReadOnlyList<string>

Methods

Answer(RecordedRequest)

Answers a request a StubHandler received: the operation from X-Amz-Target, and the body as sent.

public StubResponse Answer(RecordedRequest request)

Parameters

request RecordedRequest

The request.

Returns

StubResponse

The answer, with x-amzn-RequestId. It fits any operation, because it is the answer to the operation of the request.

Exceptions

ArgumentNullException

request is null.

Answer(string, string)

Answers a request given as text, which is what a server in front of the fake or a raw client calls.

public StubResponse Answer(string operation, string requestJson)

Parameters

operation string

The operation: GetItem, or the whole target, DynamoDB_20120810.GetItem.

requestJson string

The body.

Returns

StubResponse

The answer.

Exceptions

ArgumentNullException

An argument is null.

CreateClient(Func<DynamoDbClientOptions, DynamoDbClientOptions>?)

Creates a client over a new handler of this fake, as Create(HttpMessageHandler, Func<DynamoDbClientOptions, DynamoDbClientOptions>?) makes it: us-east-1, static fake credentials, one attempt per call.

public DynamoDbClient CreateClient(Func<DynamoDbClientOptions, DynamoDbClientOptions>? configure = null)

Parameters

configure Func<DynamoDbClientOptions, DynamoDbClientOptions>

Adjusts the options with a with expression, or null to keep them.

Returns

DynamoDbClient

The client.

Exceptions

ArgumentNullException

configure returns null.

CreateHandler()

Creates a handler that answers every request from this fake, for Create(HttpMessageHandler, Func<DynamoDbClientOptions, DynamoDbClientOptions>?) or for HttpHandler.

public StubHandler CreateHandler()

Returns

StubHandler

A new StubHandler whose responder is Answer(RecordedRequest). It holds nothing that needs disposing.

CreateStreamsClient(Func<DynamoDbClientOptions, DynamoDbClientOptions>?)

Creates a client of DynamoDB Streams over a new handler of this fake, as CreateStreams(HttpMessageHandler, Func<DynamoDbClientOptions, DynamoDbClientOptions>?) makes it: it reads the streams of the tables of this fake, and ReadShardAsync(ReadShardRequest, CancellationToken) and ReadStreamAsync(ReadStreamRequest, CancellationToken) work over it as over DynamoDB.

public DynamoDbStreamsClient CreateStreamsClient(Func<DynamoDbClientOptions, DynamoDbClientOptions>? configure = null)

Parameters

configure Func<DynamoDbClientOptions, DynamoDbClientOptions>

Adjusts the options with a with expression, or null to keep them.

Returns

DynamoDbStreamsClient

The client.

Remarks

Its TimeProvider is the clock of this fake, so that what the client waits for the stream, EmptyPageDelay and the looks for the children of a shard, passes as the fake's iterators expire, its records age and its late children are listed: a test that moves the clock moves both. configure can set another. CreateClient(Func<DynamoDbClientOptions, DynamoDbClientOptions>?) keeps the system clock, because its client waits only for itself, between the runs of RetryOnConflictAsync, which a test should not have to move a clock for.

Those waits also end at the next change of a stream of this fake: a record, a split, a stream enabled or disabled, a Reset(). So a reader that found nothing asks again as soon as a write of the test is there, and a reading ends as soon as the stream is disabled, without moving any clock: a test on the system clock reads a write in milliseconds and not after EmptyPageDelay. The reader sends the same requests as against DynamoDB, an empty page, a wait and the next page, and only the wait is shorter. A late child listed by the clock alone, see SplitStreamShards(string, int, TimeSpan), is found after the whole wait, as a retry after a failure, see FailNext(string, StubResponse, int, Func<RecordedRequest, bool>?), is sent after its whole backoff.

Exceptions

ArgumentNullException

configure returns null.

CreateTable(CreateTableRequest)

Creates a table, as a CreateTable request does, with the same checks. It is ACTIVE at once.

public TableDescription CreateTable(CreateTableRequest request)

Parameters

request CreateTableRequest

The request, as the request API takes it.

Returns

TableDescription

The description, as DescribeTable gives it.

Exceptions

ArgumentNullException

request is null.

ResourceInUseException

A table of that name exists.

ValidationException

DynamoDB would reject the request.

DynamoDbException

The request asks for what the fake does not implement, such as tags: ErrorCode is UnsupportedByFakeException.

CreateTable(string, string, string?)

Creates a table without a model, on demand, whose keys are strings: the table of client.Table(name, partitionKey, sortKey), such as the table of a LockTable of DynaSharp.Coordination. A table with keys of other types, indexes or a stream takes CreateTable(CreateTableRequest).

public TableDescription CreateTable(string tableName, string partitionKey, string? sortKey = null)

Parameters

tableName string

The table.

partitionKey string

The attribute name of the partition key, a string.

sortKey string

The attribute name of the sort key, a string, or null for a table without one.

Returns

TableDescription

The description.

Exceptions

ArgumentNullException

tableName or partitionKey is null.

ResourceInUseException

A table of that name exists.

ValidationException

DynamoDB would reject the request, such as for an empty name or a sort key named as the partition key.

CreateTable<T>()

Creates the table an entity declares, as CreateTable<T>(string) does under that name: Table, or the default of its assembly.

public TableDescription CreateTable<T>() where T : IDynamoDbEntity<T>

Returns

TableDescription

The description.

Type Parameters

T

The entity, which declares its table.

Remarks

The name as declared. The fake knows no TableNaming, so a client of the fake whose options set one opens another table: such a test passes the name it expects to CreateTable<T>(string).

Exceptions

InvalidOperationException

T declares no table.

ResourceInUseException

A table of that name exists.

CreateTable<T>(string)

Creates the table an entity lives in, as client.Table<T>(tableName).CreateTable() would: its keys with their types and every index it declares, on demand, and time to live ENABLED on the attribute of its TimeToLiveAttribute, so ExpireItems() sweeps it. A table of several entities takes CreateTable(CreateTableRequest) with the request of CreateTable().Including<TOther>().ToRequest(), which holds no time to live, and then UpdateTimeToLive through a client of the fake for one.

public TableDescription CreateTable<T>(string tableName) where T : IDynamoDbEntity<T>

Parameters

tableName string

The table.

Returns

TableDescription

The description.

Type Parameters

T

The entity.

Exceptions

ArgumentNullException

tableName is null.

ResourceInUseException

A table of that name exists.

Delete(string, Item)

Deletes an item by its key, as a DeleteItem without a condition does.

public bool Delete(string tableName, Item key)

Parameters

tableName string

The table.

key Item

The key attributes, and nothing else.

Returns

bool

true if there was an item.

Exceptions

ArgumentNullException

An argument is null.

ResourceNotFoundException

There is no such table.

ValidationException

The key does not match the schema of the table.

DeleteTable(string)

Deletes a table with its items. The name is free at once.

public void DeleteTable(string tableName)

Parameters

tableName string

The table.

Exceptions

ArgumentNullException

tableName is null.

ResourceNotFoundException

There is no such table.

ValidationException

The table is protected against deletion.

DescribeTable(string)

Describes a table, as DescribeTable does, with its item count and size as they are now.

public TableDescription DescribeTable(string tableName)

Parameters

tableName string

The table.

Returns

TableDescription

The description.

Exceptions

ArgumentNullException

tableName is null.

ResourceNotFoundException

There is no such table.

DisableStream(string)

Disables the stream of a table, as an UpdateTable with StreamSpecification StreamEnabled: false does: its open shards close, so a reader of a shard reaches its end, DescribeStream says DISABLED, and ReadStreamAsync ends once it has read every shard. The stream stays readable for 24 hours on the clock of the fake, and the table keeps naming it in LatestStreamArn.

public TableDescription DisableStream(string tableName)

Parameters

tableName string

The table, whose stream is enabled.

Returns

TableDescription

The description, as DescribeTable gives it, without StreamSpecification.

Exceptions

ArgumentNullException

tableName is null.

ResourceNotFoundException

There is no such table.

ValidationException

The table has no enabled stream.

EnableStream(string, StreamViewType)

Enables a stream on a table, as an UpdateTable with StreamSpecification StreamEnabled: true does: a new stream with one open shard, a new LatestStreamArn and a new LatestStreamLabel, also on a table whose stream was enabled before. The writes from then on are its records.

public TableDescription EnableStream(string tableName, StreamViewType viewType)

Parameters

tableName string

The table, which has no enabled stream.

viewType StreamViewType

What a record holds of the item.

Returns

TableDescription

The description, as DescribeTable gives it, with the new stream in LatestStreamArn.

Exceptions

ArgumentNullException

tableName is null.

ArgumentOutOfRangeException

viewType is Unspecified or undefined.

ResourceNotFoundException

There is no such table.

ValidationException

The table has an enabled stream: another view type takes DisableStream(string) first, as on DynamoDB.

ExpireItems()

Deletes every item that has expired: in each table with time to live ENABLED, an item whose attribute of time to live is a number of epoch seconds at or before the clock of the fake. An expiry more than five years before the clock is left, as DynamoDB leaves it. Nothing expires by itself; this is the background sweep of DynamoDB, run when the test says. A table with a stream records each delete as a REMOVE whose userIdentity is the service, dynamodb.amazonaws.com of type Service, as DynamoDB records it.

public int ExpireItems()

Returns

int

The number of items deleted.

FailNext(string, StubResponse, int, Func<RecordedRequest, bool>?)

Makes the next requests of an operation fail: fake.FailNext("GetRecords", StubResponse.DynamoDbError("ThrottlingException", "Rate exceeded")). The request gets failure and the fake never sees it, as DynamoDB never sees a request it throttles, so a failed write writes nothing; the requests after it are answered by the fake as before.

public void FailNext(string operation, StubResponse failure, int times = 1, Func<RecordedRequest, bool>? when = null)

Parameters

operation string

The operation, GetRecords, or its whole target, DynamoDBStreams_20120810.GetRecords.

failure StubResponse

The answer, such as DynamoDbError(string, string, HttpStatusCode, string?). A response of another operation fails the request, as a queued one does on a StubHandler.

times int

How many requests get it, one by default.

when Func<RecordedRequest, bool>

Which requests of the operation count, such as those of one table, request => request.BodyJson.GetProperty("TableName").GetString() == "Orders", or null for all. It runs under the lock of the fake, so it must not call the fake.

Remarks

It holds for every handler of this fake, those made before the call too, so for the clients of CreateClient(Func<DynamoDbClientOptions, DynamoDbClientOptions>?) and CreateStreamsClient(Func<DynamoDbClientOptions, DynamoDbClientOptions>?) alike, and for Answer(RecordedRequest) and Answer(string, string). A response queued with Enqueue(StubResponse) on a handler still comes first: the handler asks the fake only when its queue is empty. Requests of several threads, such as the shard readers of ReadStreamAsync, take the failures one at a time under the lock of the fake, so each failure is taken once.

Failures are taken in the order planned: a request takes the first one of its operation that when accepts. A client repeats a throttling error or a server error as it does on DynamoDB, so a failure planned for more requests than the client has attempts surfaces, and ReadStreamAsync reads a shard on after it. Reset() drops the failures not taken, and ThrowIfFailuresLeft() tells a test that the code sent fewer requests than it planned for. The direct API, such as Put(string, Item), is no request and takes none.

Exceptions

ArgumentNullException

operation or failure is null.

ArgumentException

operation is empty.

ArgumentOutOfRangeException

times is less than one.

Get(string, Item)

Gets an item by its key, as a GetItem does.

public Item? Get(string tableName, Item key)

Parameters

tableName string

The table.

key Item

The key attributes, and nothing else.

Returns

Item

A copy of the item, or null if there is none.

Exceptions

ArgumentNullException

An argument is null.

ResourceNotFoundException

There is no such table.

ValidationException

The key does not match the schema of the table.

Get<T>(Item)

Gets an item by its key from the table the entity declares, as Get<T>(string, Item) does.

public T? Get<T>(Item key) where T : IDynamoDbEntity<T>

Parameters

key Item

The key attributes, such as StubResponse.KeyOf(Customer.Keys.Of("alice")).

Returns

T

The entity, or the default of T if there is no item: null for a class.

Type Parameters

T

The entity, which declares its table.

Exceptions

ArgumentNullException

key is null.

InvalidOperationException

T declares no table.

ResourceNotFoundException

There is no such table.

ValidationException

The key does not match the schema of the table.

DynamoDbMappingException

The item is not one the entity can read, also when it is of another entity.

Get<T>(string, Item)

Gets an item by its key and reads it as an entity.

public T? Get<T>(string tableName, Item key) where T : IDynamoDbDocument<T>

Parameters

tableName string

The table.

key Item

The key attributes, such as StubResponse.KeyOf(Customer.Keys.Of("alice")).

Returns

T

The entity, or the default of T if there is no item: null for a class.

Type Parameters

T

A generated entity or document.

Remarks

A key names one item, so an item of another entity on that key is a mistake of the test and throws, as Get(key).ItemAsync() of a client throws for it. Unlike Items<T>(string), it does not skip it.

Exceptions

ArgumentNullException

An argument is null.

ResourceNotFoundException

There is no such table.

ValidationException

The key does not match the schema of the table.

DynamoDbMappingException

The item is not one the entity can read, also when it is of another entity.

Items(string)

Gets every item of a table, in key order: partitions in the order of their keys, then sort keys in the order of DynamoDB. A test should not depend on the order of partitions, as on DynamoDB.

public IReadOnlyList<Item> Items(string tableName)

Parameters

tableName string

The table.

Returns

IReadOnlyList<Item>

Copies of the items, a snapshot.

Exceptions

ArgumentNullException

tableName is null.

ResourceNotFoundException

There is no such table.

Items<T>()

Gets the items of the entity from the table it declares, as Items<T>(string) does.

public IReadOnlyList<T> Items<T>() where T : IDynamoDbEntity<T>

Returns

IReadOnlyList<T>

The entities, a snapshot.

Type Parameters

T

The entity, which declares its table.

Exceptions

InvalidOperationException

T declares no table.

ResourceNotFoundException

There is no such table.

DynamoDbMappingException

An item of the entity, or with a type without a discriminator any item, is not one the type can read.

Items<T>(string)

Gets the items of a table that are of an entity, in the order of Items(string), read as entities. On a table of several entities it gives those of T and skips the others, as a typed Scan() does with its filter on the discriminator.

public IReadOnlyList<T> Items<T>(string tableName) where T : IDynamoDbDocument<T>

Parameters

tableName string

The table.

Returns

IReadOnlyList<T>

The entities, a snapshot.

Type Parameters

T

A generated entity or document.

Remarks

For an entity with a discriminator, an item is of it when its discriminator attribute, _type or the one the entity names, is a string equal to the discriminator, which is what Is<T>() tests. Every other item is skipped: one of another entity, and one without the attribute, which a scan of the client does not return either. An item that is of the entity and that it cannot read throws, because that is a bug of the model or of a writer.

For an entity without a discriminator, with [Discriminator(OptOut = true)], and for a document, nothing tells the items apart, so every item is read, and one that the type cannot read throws.

Exceptions

ArgumentNullException

tableName is null.

ResourceNotFoundException

There is no such table.

DynamoDbMappingException

An item of the entity, or with a type without a discriminator any item, is not one the type can read.

Put(string, Item)

Puts an item, as a PutItem without a condition does: it replaces the item with its key, after the checks of the keys, the keys of the indexes and the size, and the indexes follow.

public void Put(string tableName, Item item)

Parameters

tableName string

The table.

item Item

The item, which the fake copies.

Exceptions

ArgumentNullException

An argument is null.

ResourceNotFoundException

There is no such table.

ValidationException

DynamoDB would reject the item: a key attribute is missing or of another type, a number is out of range, the item is larger than 400 KB.

Put<T>(string, T)

Puts an entity, as a PutItem of From<T>(T) does: with the keys, the discriminator and the version the entity writes. A version is stored as the entity holds it, not raised as a Put command raises it.

public void Put<T>(string tableName, T entity) where T : IDynamoDbDocument<T>

Parameters

tableName string

The table.

entity T

The entity.

Type Parameters

T

A generated entity or document.

Exceptions

ArgumentNullException

An argument is null.

ResourceNotFoundException

There is no such table.

ValidationException

DynamoDB would reject the item.

Put<T>(T)

Puts an entity into the table it declares, as Put<T>(string, T) does.

public void Put<T>(T entity) where T : IDynamoDbEntity<T>

Parameters

entity T

The entity.

Type Parameters

T

The entity, which declares its table.

Exceptions

ArgumentNullException

entity is null.

InvalidOperationException

T declares no table.

ResourceNotFoundException

There is no such table.

ValidationException

DynamoDB would reject the item.

Reset()

Drops every table with its items and streams, forgets every client request token, and drops the failures planned with FailNext(string, StubResponse, int, Func<RecordedRequest, bool>?) that no request took. The handlers and clients of this fake stay valid and see no tables. A stream enabled afterwards gets another ARN than the streams before, also on the same clock, so a shard iterator from before is ResourceNotFoundException.

public void Reset()

SplitStreamShards(string, int, TimeSpan)

Ends every open shard of the stream of a table and opens its children, as DynamoDB does on its own: one child when it rolls a shard over, which it does every few hours, two when it splits a shard with its partition. The children take the records of the shard from then on, each the items of its share of the partition keys, and name it in ParentShardId. The ended shard gets its EndingSequenceNumber, and a reader of it reaches its end.

public void SplitStreamShards(string tableName, int children = 2, TimeSpan secondChildListedAfter = default)

Parameters

tableName string

The table, whose stream is enabled.

children int

How many children each open shard gets: 1 or 2, the most a shard of DynamoDB has.

secondChildListedAfter TimeSpan

How long after the split the second child is listed; zero, the default, lists it at once. It applies only to two children.

Remarks

DynamoDB can list a child in DescribeStream a moment after its parent ended. secondChildListedAfter plays that for the second of two children: it takes records at once and is listed, also by a ShardFilter of CHILD_SHARDS, only once the clock of the fake has moved on by that much.

Exceptions

ArgumentNullException

tableName is null.

ArgumentOutOfRangeException

children is not 1 or 2, or secondChildListedAfter is negative.

ResourceNotFoundException

There is no such table.

ValidationException

The table has no enabled stream.

ThrowIfFailuresLeft()

Throws when a failure planned with FailNext(string, StubResponse, int, Func<RecordedRequest, bool>?) was not taken, naming the operation and how many are left.

public void ThrowIfFailuresLeft()

Exceptions

InvalidOperationException

A failure is left: the code under test sent fewer requests than the test planned failures for.