Testing Email¶
TestEmailService is a built-in test double for IEmailService with call recording, configurable responses, and exception injection. No mocking framework needed.
Setup¶
var emailService = new TestEmailService();
var runtime = TestAppRuntime.Create()
.WithEmailService(emailService);
Or use the auto-configured runtime (wires TestEmailService automatically):
Call Recording¶
Every SendAsync and SendBatchAsync call is recorded:
await program.RunAsync(runtime);
// Assert individual sends
await Assert.That(emailService.SendCalls).HasCount().EqualTo(1);
await Assert.That(emailService.SendCalls[0].Subject).Contains("Welcome");
await Assert.That(emailService.SendCalls[0].To[0].Value).IsEqualTo("alice@example.com");
// Assert batch sends
await Assert.That(emailService.BatchCalls).HasCount().EqualTo(1);
await Assert.That(emailService.BatchCalls[0]).HasCount().EqualTo(10);
// Combined totals
await Assert.That(emailService.TotalSendCount).IsEqualTo(11);
await Assert.That(emailService.AllMessages).HasCount().EqualTo(11);
Recording Properties¶
| Property | Type | Description |
|---|---|---|
SendCalls |
IReadOnlyList<EmailMessage> |
Individual SendAsync calls |
BatchCalls |
IReadOnlyList<IReadOnlyList<EmailMessage>> |
Each SendBatchAsync call (the batch) |
AllMessages |
IReadOnlyList<EmailMessage> |
All messages combined (individual + batch) |
TotalSendCount |
int |
Total messages sent across all calls |
Custom Responses¶
Override the default "accepted" response:
emailService.OnSend = msg => new EmailResult
{
Id = EmailMessageId.New(),
ProviderMessageId = "custom-123",
Status = EmailStatus.Rejected,
ErrorCode = "INVALID_ADDRESS",
ErrorMessage = "Recipient address does not exist"
};
For batch sends:
emailService.OnSendBatch = messages => messages.Select(m => new EmailResult
{
Id = EmailMessageId.New(),
ProviderMessageId = $"batch-{Guid.NewGuid():N}",
Status = m.To[0].Value.Contains("invalid") ? EmailStatus.Rejected : EmailStatus.Accepted
}).ToList();
Exception Injection¶
Test error handling by making sends throw:
emailService.SendException = new HttpRequestException("Provider unavailable");
var result = await program.RunAsync(runtime);
await Assert.That(result).IsFail();
Reset¶
Clear all recorded state between test phases:
emailService.Reset();
// SendCalls, BatchCalls cleared
// OnSend, OnSendBatch, SendException reset to null
Full Example¶
[Test]
public async Task OrderConfirmation_SendsEmail()
{
var emailService = new TestEmailService();
var runtime = TestAppRuntime.Create()
.WithEmailService(emailService)
.WithStubOrderStore(stub => stub
.OnGetById(_ => Task.FromResult<Order?>(TestData.SampleOrder)));
await OrderWorkflows.ConfirmOrder(TestData.OrderId).RunAsync(runtime);
await Assert.That(emailService.SendCalls).HasCount().EqualTo(1);
var sent = emailService.SendCalls[0];
await Assert.That(sent.Subject).Contains("Order #");
await Assert.That(sent.To[0]).IsEqualTo(new EmailAddress("customer@example.com"));
await Assert.That(sent.Tags!["order_id"]).IsEqualTo(TestData.OrderId.ToString());
}