E-Mail-Tests mit Mailpit und .NET Aspire
Wann diese Fähigkeit verwendet werden sollte
Verwenden Sie diese Fähigkeit, wenn:
- Sie E-Mail-Zustellung lokal testen möchten, ohne echte E-Mails zu senden.
- Sie E-Mail-Infrastruktur in .NET Aspire einrichten.
- Sie Integrationstests schreiben, die den Versand von E-Mails überprüfen.
- Sie HTML-Rendering und E-Mail-Header debuggen.
Verwandte Fähigkeiten:
aspnetcore/mjml-email-templates- MJML-Vorlagenbearbeitungtesting/verify-email-snapshots- Snapshot-Test von gerendertem HTMLaspire/integration-testing- Allgemeine Aspire-Testmuster
Was ist Mailpit?
Mailpit ist ein leichtgewichtiges E-Mail-Testtool, das:
- Den gesamten SMTP-Verkehr abfängt, ohne E-Mails zuzustellen
- Eine Weboberfläche zur Ansicht gefangener E-Mails bereitstellt
- Eine API für den programmatischen Zugriff bereitstellt
- HTML-Rendering, Header und Anhänge unterstützt
Ideal für Entwicklung und Integrationstests.
Aspire AppHost-Konfiguration
Fügen Sie Mailpit als Container in Ihrer AppHost hinzu:
// AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
// Mailpit für E-Mail-Tests hinzufügen
var mailpit = builder.AddContainer("mailpit", "axllent/mailpit")
.WithHttpEndpoint(port: 8025, targetPort: 8025, name: "ui")
.WithEndpoint(port: 1025, targetPort: 1025, name: "smtp");
// Referenz im API-Projekt
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WithReference(mailpit.GetEndpoint("smtp"))
.WithEnvironment("Smtp__Host", mailpit.GetEndpoint("smtp"));
builder.Build().Run();
SMTP-Konfiguration
appsettings.json
{
"Smtp": {
"Host": "localhost",
"Port": 1025,
"EnableSsl": false,
"FromAddress": "[email protected]",
"FromName": "MyApp"
}
}
Konfigurationsklasse
public class SmtpSettings
{
public string Host { get; set; } = "localhost";
public int Port { get; set; } = 1025;
public bool EnableSsl { get; set; } = false;
public string FromAddress { get; set; } = "[email protected]";
public string FromName { get; set; } = "MyApp";
// Optional: Für Produktions-SMTP
public string? Username { get; set; }
public string? Password { get; set; }
}
Dienstregistrierung
// In Program.cs oder einer Erweiterungsmethode
services.Configure<SmtpSettings>(configuration.GetSection("Smtp"));
services.AddSingleton<IEmailSender>(sp =>
{
var settings = sp.GetRequiredService<IOptions<SmtpSettings>>().Value;
return new SmtpEmailSender(settings);
});
Implementierung des E-Mail-Versands
public interface IEmailSender
{
Task SendEmailAsync(EmailMessage message, CancellationToken ct = default);
}
public sealed class SmtpEmailSender : IEmailSender
{
private readonly SmtpSettings _settings;
public SmtpEmailSender(SmtpSettings settings)
{
_settings = settings;
}
public async Task SendEmailAsync(EmailMessage message, CancellationToken ct = default)
{
using var client = new SmtpClient();
await client.ConnectAsync(
_settings.Host,
_settings.Port,
_settings.EnableSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.None,
ct);
if (!string.IsNullOrEmpty(_settings.Username))
{
await client.AuthenticateAsync(_settings.Username, _settings.Password, ct);
}
var mailMessage = new MimeMessage();
mailMessage.From.Add(new MailboxAddress(_settings.FromName, _settings.FromAddress));
mailMessage.To.Add(new MailboxAddress(message.ToName, message.To));
mailMessage.Subject = message.Subject;
var bodyBuilder = new BodyBuilder { HtmlBody = message.HtmlBody };
mailMessage.Body = bodyBuilder.ToMessageBody();
await client.SendAsync(mailMessage, ct);
await client.DisconnectAsync(true, ct);
}
}
Erfordert das NuGet-Paket MailKit:
dotnet add package MailKit
Anzeigen gefangener E-Mails
Weboberfläche
Navigieren Sie zu http://localhost:8025, um folgendes zu sehen:
- Posteingang - Alle gefangenen E-Mails
- HTML-Ansicht - Gerenderte E-Mail
- Quellansicht - Roh-HTML/MJML
- Header - Vollständige E-Mail-Header
- Anhänge - Alle angehängten Dateien
Aspire Dashboard
Der Mailpit-UI-Endpunkt wird im Aspire-Dashboard unter Ressourcen angezeigt.
Integrationstests
Test-Framework mit Aspire
public class EmailIntegrationTests : IClassFixture<AspireFixture>
{
private readonly HttpClient _client;
private readonly MailpitClient _mailpit;
public EmailIntegrationTests(AspireFixture fixture)
{
_client = fixture.CreateClient();
_mailpit = new MailpitClient(fixture.GetMailpitUrl());
}
[Fact]
public async Task SignupFlow_SendsWelcomeEmail()
{
// Arrange
await _mailpit.ClearMessagesAsync();
// Act - Signup-Vorgang auslösen
var response = await _client.PostAsJsonAsync("/api/auth/signup", new
{
Email = "[email protected]",
Password = "SecurePassword123!"
});
response.EnsureSuccessStatusCode();
// Assert - Überprüfen, ob E-Mail gesendet wurde
var messages = await _mailpit.GetMessagesAsync();
var welcomeEmail = messages.Should().ContainSingle()
.Which;
welcomeEmail.To.Should().Contain("[email protected]");
welcomeEmail.Subject.Should().Contain("Welcome");
welcomeEmail.HtmlBody.Should().Contain("Thank you for signing up");
}
}
Mailpit API-Client
public class MailpitClient
{
private readonly HttpClient _client;
public MailpitClient(string baseUrl)
{
_client = new HttpClient { BaseAddress = new Uri(baseUrl) };
}
public async Task<List<MailpitMessage>> GetMessagesAsync()
{
var response = await _client.GetFromJsonAsync<MailpitResponse>("/api/v1/messages");
return response?.Messages ?? new List<MailpitMessage>();
}
public async Task ClearMessagesAsync()
{
await _client.DeleteAsync("/api/v1/messages");
}
public async Task<MailpitMessage?> WaitForMessageAsync(
Func<MailpitMessage, bool> predicate,
TimeSpan timeout)
{
var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
var messages = await GetMessagesAsync();
var match = messages.FirstOrDefault(predicate);
if (match != null)
return match;
await Task.Delay(100);
}
return null;
}
}
public class MailpitResponse
{
public List<MailpitMessage> Messages { get; set; } = new();
}
public class MailpitMessage
{
public string Id { get; set; } = "";
public List<string> To { get; set; } = new();
public string Subject { get; set; } = "";
public string HtmlBody { get; set; } = "";
}
Aspire Test-Framework
public class AspireFixture : IAsyncLifetime
{
private DistributedApplication? _app;
private string _mailpitUrl = "";
public async Task InitializeAsync()
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.MyApp_AppHost>();
// Persistenz für saubere Tests deaktivieren
appHost.Configuration["MyApp:UseVolumes"] = "false";
_app = await appHost.BuildAsync();
await _app.StartAsync();
// Mailpit-URL von Aspire abrufen
var mailpit = _app.GetContainerResource("mailpit");
_mailpitUrl = await mailpit.GetEndpointAsync("ui");
}
public HttpClient CreateClient()
{
var api = _app!.GetProjectResource("api");
return api.CreateHttpClient();
}
public string GetMailpitUrl() => _mailpitUrl;
public async Task DisposeAsync()
{
if (_app != null)
await _app.DisposeAsync();
}
}
Häufige Testmuster
Auf asynchrone E-Mail warten
Manche E-Mails werden asynchron versendet. Warten Sie darauf:
[Fact]
public async Task AsyncWorkflow_EventuallySendsEmail()
{
await _mailpit.ClearMessagesAsync();
// Asynchronen Workflow auslösen
await _client.PostAsync("/api/workflows/start", null);
// Auf E-Mail warten (mit Timeout)
var email = await _mailpit.WaitForMessageAsync(
m => m.Subject.Contains("Workflow Complete"),
timeout: TimeSpan.FromSeconds(10));
email.Should().NotBeNull();
}
Mehrere E-Mails überprüfen
[Fact]
public async Task BulkOperation_SendsMultipleEmails()
{
await _mailpit.ClearMessagesAsync();
await _client.PostAsJsonAsync("/api/invitations/bulk", new
{
Emails = new[] { "[email protected]", "[email protected]", "[email protected]" }
});
var messages = await _mailpit.WaitForMessagesAsync(
expectedCount: 3,
timeout: TimeSpan.FromSeconds(10));
messages.Should().HaveCount(3);
messages.Select(m => m.To.First())
.Should().BeEquivalentTo("[email protected]", "[email protected]", "[email protected]");
}
E-Mail-Inhalt überprüfen
[Fact]
public async Task PasswordReset_ContainsValidResetLink()
{
await _mailpit.ClearMessagesAsync();
await _client.PostAsJsonAsync("/api/auth/forgot-password", new
{
Email = "[email protected]"
});
var email = await _mailpit.WaitForMessageAsync(
m => m.Subject.Contains("Password Reset"),
timeout: TimeSpan.FromSeconds(5));
// Reset-Link aus HTML extrahieren
var resetLink = Regex.Match(email!.HtmlBody, @"href=""([^""]+/reset/[^""]+)""")
.Groups[1].Value;
resetLink.Should().StartWith("https://myapp.com/reset/");
// Überprüfen, ob der Link funktioniert
var resetResponse = await _client.GetAsync(resetLink);
resetResponse.StatusCode.Should().Be(HttpStatusCode.OK);
}
Produktion vs. Entwicklung
services.AddSingleton<IEmailSender>(sp =>
{
var settings = sp.GetRequiredService<IOptions<SmtpSettings>>().Value;
var env = sp.GetRequiredService<IHostEnvironment>();
if (env.IsDevelopment())
{
// Mailpit - keine Authentifizierung, kein SSL
return new SmtpEmailSender(settings);
}
else
{
// Produktions-SMTP (SendGrid, Postmark, etc.)
return new SmtpEmailSender(settings with
{
EnableSsl = true
});
}
});
Fehlerbehebung
E-Mails erscheinen nicht
- Überprüfen Sie, ob der Mailpit-Container im Aspire-Dashboard läuft
- Überprüfen Sie die SMTP-Host/Port-Konfiguration
- Suchen Sie nach Ausnahmen in den Anwendungsprotokollen
Verbindung abgelehnt
# Überprüfen, ob Mailpit lauscht
curl http://localhost:8025/api/v1/messages
Aspire-Endpunkt wird nicht aufgelöst
// Sicherstellen, dass die Endpunktreferenz korrekt ist
.WithEnvironment("Smtp__Host", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Host))
.WithEnvironment("Smtp__Port", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Port))
Ressourcen
- Mailpit: https://github.com/axllent/mailpit
- Mailpit API: https://mailpit.axllent.org/docs/api-v1/
- MailKit: https://github.com/jstedfast/MailKit
- Aspire Containers: https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/container-resources