This library provides in-memory SDK for Azure Event Hubs which can be used as a drop-in replacement for the official Azure.Messaging.EventHubs SDK in your tests.
Example Usage | Fault Injection | Features | Available APIs
Tip
See the whole In-Memory Azure Test SDK suite if you are interested in other Azure services.
To get started, add Spotflow.InMemory.Azure.EventHubs
package to your project.
dotnet add Spotflow.InMemory.Azure.EventHubs
This package provides in-memory implementation of Azure Event Hubs SDK clients and models.
These in-memory implementations are inheriting the real Azure SDK types so you can use them as a drop-in replacement in your tests.
There is nothing special about the in-memory types, so they can be injected in many ways, e.g. via DI and constructor injection as demonstrated below.
Only extra step is to create parent InMemoryEventHubProvider
instance for the in-memory clients.
Let's consider the following type ExampleService
as an example:
class ExampleService(EventHubProducerClient producerClient)
{
public async Task SendEventAsync(BinaryData payload)
{
await producerClient.SendAsync(new EventData(payload));
}
}
The ExampleService
might be constructed, for example, using DI:
// Setup DI - production configuration
var connectionString = "Endpoint=sb://test-namespace...;EntityPath=test-eh;...";
var services = new ServiceCollection();
services.AddSingleton<EventHubProducerClient>(new EventHubProducerClient(connectionString));
services.AddSingleton<ExampleService>();
...
// Use resulting service provider
var service = services.BuildServiceProvider().GetRequiredService<ExampleService>();
Note:
Most frequently, the new ServiceCollection()
and .BuildServiceProvider()
will called by ASP.NET or other frameworks.
This is just an example of one of many ways how the in-memory clients can be used.
To inject the in-memory implementation of EventHubProducerClient
to the ExampleService
during test,
the InMemoryEventHubProducerClient
can be simply substituted for the real EventHubProducerClient
in the DI container:
// Setup DI - test-only configuration (additive)
var inMemoryProvider = new InMemoryEventHubProvider();
var producerClient = new InMemoryEventHubProducerClient(connectionString, inMemoryProvider);
services.AddSingleton<EventHubProducerClient>(producerClient);
By default, the InMemoryEventHubProvider
is empty but exposes methods that allow to set up expected management-plane state:
inMemoryProvider.AddNamespace("test-namespace...").AddEventHub("test-eh");
To send/receive events in test code (data-plane operations), the in-memory clients can be used directly:
await using var partitionReceiver = new InMemoryPartitionReceiver(consumerGroup, partitionId, EventPosition.Earliest, connectionString);
await using var producerClient = new InMemoryEventHubProducerClient(connectionString, inMemoryProvider);
await producerClient.SendAsync(new EventData(...));
var receivedEvents = await partitionReceiver.ReceiveBatchAsync(10, TimeSpan.FromSeconds(5));
Fault injections let you simulate transient and persistent faults in Azure Event Hub. Thanks to that you can test how your application behaves in case of Azure outages, network issues, timeouts, etc.
To inject a fault, you need to use the concept of hooks - functions that are called before or after the actual operation is executed.
A new hook can be registered by calling the AddHook
method on the InMemoryEventHubProvider
instance.
You can build fault hook by calling the Faults
method on the hook context and then calling the appropriate method, e.g. ServiceIsBusy
:
For overview of available hooks, please see the Hooks section.
var provider = new InMemoryEventHubProvider();
var hook = provider.AddHook(hookBuilder => hookBuilder.Before(ctx => ctx.Faults().ServiceIsBusy()));
The AddHook
method gives you a builder that lets you define which operations the hook should apply to.
In the example above, the hook affects all Event Hub operations.
However, you can limit it to specific operations, like Send
, or target specific scopes, such as operation on Event Hub called my-eventhub
:
var hook = provider.AddHook(hookBuilder => hookBuilder.ForProducer(eventHubName: "my-event-hub")
.BeforeSend(ctx => ctx.Faults().ServiceIsBusy()));
You can control when the hook should execute via the IHookRegistration
interface returned by the AddHook
method.
By default, the hook is enabled, but you can disable it by calling the Disable
method.
To simulate temporary outages, use the DisableAfter
method to limit the number of fault occurrences.
See a full example of fault injection below:
var provider = new InMemoryEventHubProvider();
var hook = provider.AddHook(hook => hook.Before(ctx => ctx.Faults().ServiceIsBusy()));
var eventHub = provider.AddNamespace("test-ns").AddEventHub("test-eh", 1);
var producerClient = InMemoryEventHubProducerClient.FromEventHub(eventHub);
var act = () => producerClient.SendAsync([new EventData()]);
await act.Should().ThrowAsync<EventHubsException>().WithMessage("Event hub 'test-eh' in namespace 'test-ns' is busy. (test-eh). *");
hook.Disable();
await act.Should().NotThrowAsync();
Delay simulation is currently not supported for Azure Event Hub.
However, hooks can be used to simulate custom delays. For overview of available hooks, please see the Hooks section.
Following SDK clients and their method groups and properties are supported.
Async versions of these methods are also supported. All supported async methods are guaranteed executed truly asynchronously by using Task.Yield().
Other methods and properties are not supported and will throw NotSupportedException
.
Clients are thread-safe.
Property | Note |
---|---|
EventHubName |
|
FullyQualifiedNamespace |
|
Identifier |
|
IsClosed |
Method group |
---|
CloseAsync |
CreateBatchAsync |
DisposeAsync |
CreateBatchAsync |
GetEventHubPropertiesAsync |
GetPartitionIdsAsync |
GetPartitionPropertiesAsync |
Constructors & factory methods | Note |
---|---|
(string connectionString) |
|
(string fullyQualifiedNamespace, string eventHubName, TokenCredential tokenCredential) |
|
(EventHubConnection connection) |
|
FromEventHub(InMemoryEventHub eventHub) |
|
FromNamespace(InMemoryEventHubNamespace eventHubNamespace, string eventHubName) |
Property | Note |
---|---|
ConsumerGroup |
|
EventHubName |
|
FullyQualifiedNamespace |
|
Identifier |
|
IsClosed |
Method group |
---|
CloseAsync |
DisposeAsync |
GetEventHubPropertiesAsync |
GetPartitionIdsAsync |
GetPartitionPropertiesAsync |
Constructors & factory methods | Note |
---|---|
(string consumerGroup, string connectionString) |
|
(string consumerGroup, string fullyQualifiedNamespace, string eventHubName, TokenCredential tokenCredential) |
|
(string consumerGroup, EventHubConnection connection) |
|
FromEventHub(string consumerGroup, InMemoryEventHub eventHub) |
|
FromNamespace(string consumerGroup, InMemoryEventHubNamespace eventHubNamespace, string eventHubName) |
Property | Note |
---|---|
ConsumerGroup |
|
EventHubName |
|
FullyQualifiedNamespace |
|
Identifier |
|
InitialPosition |
|
IsClosed |
|
PartitionId |
Method group |
---|
GetPartitionPropertiesAsync |
ReadLastEnqueuedEventProperties |
ReceiveBatchAsync |
DisposeAsync |
CloseAsync |
Constructors & factory methods | Note |
---|---|
(string consumerGroup, string partitionId, EventPosition eventPosition, string connectionString) |
No credentials are validated. |
(string consumerGroup, string partitionId, EventPosition eventPosition, string connectionString, string eventHubName) |
No credentials are validated. |
(string consumerGroup, string partitionId, EventPosition eventPosition, string fullyQualifiedNamespace, string eventHubName, TokenCredential credential) |
No credentials are validated. |
(string consumerGroup, string partitionId, EventPosition eventPosition, EventHubConnection connection) |
No credentials are validated. |
FromNamespace(string consumerGroup, string partitionId, EventPosition startingPosition, InMemoryEventHubNamespace eventHubNamespace, string eventHubName) |
No credentials are validated. |
FromEventHub(string consumerGroup, string partitionId, EventPosition startingPosition, InMemoryEventHub eventHub) |
No credentials are validated. |
FromEventHub(string partitionId, EventPosition startingPosition, InMemoryEventHub eventHub) |
Default consumer group is used. No credentials are validated. |
For the supported methods enumerated above, not all features are fully implemented.
If the given feature is not supported, than the method will just ignore any parameters related to that feature.
Feature | Is Supported |
---|---|
Batches | ✅ |
Event System Property - Content Type | ✅ |
Event System Property - Correlation Id | ✅ |
Event System Property - Message Id | ✅ |
Event System Property - Offset (*) | ✅ |
Event System Property - Sequence Number | ✅ |
Offset-based starting positions | ❌ |
Partition keys | ✅ |
Properties - Event Hub | ✅ |
Properties - Partition | ✅ |
Randomization of initial sequence numbers for event hub partitions | ✅ |
Sequence number based starting positions (including Earliest and Latest ) |
✅ |
(*) The value of Offset
system property is based on event data size. Calculation of the the event data size is not exactly the same as for real Azure Event Hubs but should be good enough for testing purposes.
Following hooks are supported in both Before
and After
variants:
- All
Event Hub
operations- All
Producer
operationsSend
- All
Consumer
operationsReceiveBatch
GetEventHubProperties
GetPartitionProperties
- All
For details about concept of hooks, please see the Hooks page.