Builder meets Faker
Testdata-Builder, Teil 2
Im Grundlagen-Artikel „Bob, der Testdaten-Baumeister“ in der dotnetpro-Ausgabe 10-11/2025 [1] haben wir das Testdata-Builder-Pattern kennengelernt und gesehen, wie es uns hilft, wartbare und lesbare Tests zu schreiben. Die dort vorgestellten Builder verwendeten einfache Standardwerte wie „John Doe“ oder „Sample Blog Post“. Für viele Unit-Tests ist das völlig ausreichend. Doch sobald wir umfangreiche Integrationstests schreiben oder größere Datenmengen benötigen, stoßen wir an Grenzen: Wir brauchen realistische Testdaten in größeren Mengen – und zwar ohne jeden einzelnen Datensatz manuell zu konfigurieren.
Genau hier kommt die Bogus-Library ins Spiel. Bogus ist eine leistungsstarke .NET-Bibliothek zur Generierung von Fake-Daten, die auf dem populären JavaScript-Projekt Faker.js basiert. In Kombination mit unseren Testdata-Buildern und KI-Assistenten wie GitHub Copilot entsteht ein mächtiges Werkzeug für die Erstellung umfangreicher, realistischer Testsuiten.
Das Problem: Wenn einfache Builder nicht mehr ausreichen
Betrachten wir ein konkretes Szenario: Wir möchten die Pagination-Funktionalität unseres Blog-Systems testen. Dafür benötigen wir 100 Blogposts mit verschiedenen Autoren und Kategorien. Mit unseren bisherigen Buildern sähe das so aus:
var posts = new List<BlogPost>();
for (int i = 0; i < 100; i++)
{
posts.Add(new BlogPostBuilder()
.WithId(i)
.WithTitle($"Post {i}")
.WithContent($"Content {i}")
.Build());
}
Dieser Ansatz hat mehrere Nachteile:
- Unrealistische Daten: Die Testdaten sehen nicht aus wie echte Daten. Titel wie "Post 42" helfen nicht dabei, potenzielle Probleme mit Sonderzeichen, Umlauten oder unterschiedlichen Textlängen zu erkennen.
- Fehlende Vielfalt: Alle Posts haben die gleiche Struktur. In der Realität variieren Titel- und Content-Längen erheblich.
- Manueller Aufwand: Für jeden Test müssen wir die Schleife neu schreiben und anpassen.
- Schwer zu erweitern: Möchten wir zusätzlich verschiedene Autoren oder Kategorien, wird der Code schnell unübersichtlich.
Die Lösung: Bogus für realistische Testdaten
Die Bogus-Library löst diese Probleme elegant. Sie kann automatisch realistische Daten für nahezu jeden erdenklichen Datentyp generieren: Namen, E-Mail-Adressen, Texte, Daten, und vieles mehr. Zunächst installieren wir dafür das NuGet-Package:
dotnet add package Bogus
Bogus funktioniert nach einem einfachen Prinzip: Man konfiguriert einen Faker<T>, der beschreibt, wie die Eigenschaften eines Objekts befüllt werden sollen:
var faker = new Faker<Author>() .RuleFor(a => a.Id, f => f.IndexFaker) .RuleFor(a => a.Name, f => f.Name.FullName()) .RuleFor(a => a.Email, f => f.Internet.Email()); var author = faker.Generate();
Das Ergebnis ist ein Author-Objekt mit realistischen Werten wie:
- Id: 1
- Name: "Sarah Schmidt"
- Email: "sarah.schmidt47@example.com"
Darüber hinaus kann der Faker auch direkt Werte beim Aufruf erzeugen. Der Aufruf faker.Email() gibt beispielsweise bei jedem Aufruf einen anderen String zurück, der wie eine E-Mail-Adresse aussieht. Dies machen wir uns im Folgenden bei der Erweiterung unserer Testdata-Builder zunutze.
Integration von Bogus in Testdata-Builder
Die wahre Stärke entfaltet sich, wenn wir Bogus mit unseren bestehenden Testdata-Buildern kombinieren. Dadurch behalten wir alle Vorteile des Builder-Patterns – insbesondere das Fluent-API und die Möglichkeit, einzelne Werte zu überschreiben – und ergänzen sie um die automatische Generierung realistischer Daten. Listing 1 enthält den bisherigen BlogPostBuilder, wie wir ihn im Grundlagen-Artikel unter [1] definiert haben.
Listing 1: Der bisherige BlogPostBuilder
public class BlogPostBuilder
{
private int _id = 1;
private string _title = "Sample Blog Post";
private string _content = "This is a sample blog post content.";
private DateTime _publishedDate = DateTime.Now.AddDays(-1);
private Author _author = new AuthorBuilder().Build();
private Category _category = new CategoryBuilder().Build();
private bool _isPublished = true;
public BlogPostBuilder WithTitle(string title)
{
_title = title;
return this;
}
// ... weitere With-Methoden ...
public BlogPost Build()
{
return new BlogPost
{
Id = _id,
Title = _title,
Content = _content,
PublishedDate = _publishedDate,
Author = _author,
Category = _category,
IsPublished = _isPublished,
};
}
}
Nun erweitern wir diesen Builder um Bogus wie in Listing 2 gezeigt. Die wichtigen Änderungen sind:
- Nullable-Felder: Alle privaten Felder sind nun nullable (string?, int?). Dadurch können wir unterscheiden, ob ein Wert explizit gesetzt wurde oder nicht.
- Null-Coalescing-Operator: In der Build()-Methode verwenden wir ??, um entweder den explizit gesetzten Wert oder einen von Bogus generierten Wert zu verwenden.
- Faker:Faker ist die zentrale Klasse aus der Bogus-Bibliothek. Sie dient dazu, die Testdaten zu erzeugen.
- ID-Counter: Ein statischer Counter stellt sicher, dass jede generierte Instanz eine eindeutige ID erhält.
- BuildMany()-Methode: Diese neue Methode erlaubt es uns, mit einem Aufruf mehrere Instanzen zu erzeugen – ideal für Tests, die viele Objekte benötigen.
Listing 2: BlogPostBuilder mit einem Faker aus Bogus, um Testdaten zu generieren
public class BlogPostBuilder
{
private static readonly Faker _faker = new Faker("de");
private static int _idCounter = 1;
private int? _id = null;
private string? _title = null;
private string? _content = null;
private DateTime? _publishedDate = null;
private Author? _author = null;
private Category? _category = null;
private bool? _isPublished = null;
public BlogPostBuilder WithTitle(string title)
{
_title = title;
return this;
}
// ... weitere With-Methoden ...
public BlogPost Build()
{
return new BlogPost
{
Id = _id ?? _idCounter++,
Title = _title ?? _faker.Lorem.Sentence(5, 3),
Content = _content ?? _faker.Lorem.Paragraphs(3),
PublishedDate = _publishedDate ?? _faker.Date.Past(1),
Author = _author ?? new AuthorBuilder().Build(),
Category = _category ?? new CategoryBuilder().Build(),
IsPublished = _isPublished ?? _faker.Random.Bool(0.8f),
};
}
public IEnumerable<BlogPost> BuildMany(int count)
{
for (int i = 0; i < count; i++)
{
yield return Build();
}
}
}
Realistische Testdaten in Aktion
Listing 3 zeigt, wie man mit dem erweiterten Testdata-Builder elegante Tests für ein Pagination-Feature schreiben kann. Die generierten Blogposts haben dabei realistische Eigenschaften:
- Titel wie „Perspiciatis voluptas eos accusamus blanditiis“,
- mehrzeilige Lorem-Ipsum-Inhalte,
- zufällige Veröffentlichungsdaten im letzten Jahr,
- eine 80-prozentige Wahrscheinlichkeit, publiziert zu sein.
Listing 3: Ein Test für ein Pagination-Feature, bei dem 100 Blogartikel generiert werden
[Fact]
public void GetPostsPage_With100Posts_ReturnsCorrectPage()
{
// Arrange
var posts = new BlogPostBuilder().BuildMany(100).ToList();
var service = new BlogPostService();
// Act
var firstPage = service.GetPostsPage(posts, pageNumber: 1, pageSize: 10);
var lastPage = service.GetPostsPage(posts, pageNumber: 10, pageSize: 10);
// Assert
Assert.Equal(10, firstPage.Count());
Assert.Equal(10, lastPage.Count());
Assert.NotEqual(firstPage.First().Id, lastPage.First().Id);
}
Gleichzeitig behalten wir die volle Kontrolle über spezifische Testszenarien. In Listing 4 werden 50 Blogposts mit der Eigenschaft IsPublished=false und 50 Blogposts mit der Eigenschaft IsPublished=true benötigt. Dies kann ganz einfach mit den aus dem Grundlagen-Artikel unter [1] bekannten With-Methoden bewerkstelligt werden.
Listing 4: Bestimmte Daten mit With-Methoden definieren
[Fact]
public void GetPublishedPosts_WithMixedPosts_ReturnsOnlyPublished()
{
// Arrange
var publishedPosts = new BlogPostBuilder()
.WithIsPublished(true)
.BuildMany(50);
var unpublishedPosts = new BlogPostBuilder()
.WithIsPublished(false)
.BuildMany(50);
var allPosts = publishedPosts.Concat(unpublishedPosts);
var service = new BlogPostService();
// Act
var result = service.GetPublishedPosts(allPosts);
// Assert
Assert.Equal(50, result.Count());
Assert.All(result, post => Assert.True(post.IsPublished));
}
KI für Bogus-Integration nutzen
Wie in Teil 1 dieser Online-Artikelserie zu Testdata-Buildern gezeigt wurde, können KI-Assistenten wie GitHub Copilot uns dabei unterstützen, konsistente Builder zu erstellen. Mit dem richtigen Prompt können wir die KI anweisen, bestehende Builder um Bogus zu erweitern. Ein effektiver Prompt könnte lauten:
Erweitere den AuthorBuilder um Bogus-Integration. Verwende einen statischen Faker mit deutscher Locale. Alle Felder sollen nullable sein und in der Build()-Methode entweder den gesetzten Wert oder einen von Bogus generierten Wert verwenden. Füge eine BuildMany(int count)-Methode hinzu.
Noch besser: Wir erweitern die Instruktionsdatei (bei GitHub Copilot .github/copilot-instructions.md) aus Teil 1 dieser Online-Artikelserie zu Testdata-Buildern mit den Inhalt aus Listing 5. Mit diesen Anweisungen generiert die KI konsistent strukturierte Builder mit Bogus-Integration.
Listing 5: Erweiterung der Instruktionsdatei
## Bogus Integration
Beim Erweitern von Testdata-Buildern mit Bogus:
### Faker Setup
- Definiere einen statischen Faker:
`private static readonly Faker _faker = new Faker("de");`
- Verwende einen statischen Counter für IDs
### Nullable Fields
- Alle privaten Felder sind nullable (Type?)
- Ermöglicht Unterscheidung zwischen "nicht gesetzt" und "explizit gesetzt"
### Build-Methode
- Verwende Null-Coalescing-Operator (??) für jeden Wert
- Schema: `Property = _property ?? _faker.GeneratedValue()`
- Beispiele:
- String: `_faker.Lorem.Sentence()`
- Email: `_faker.Internet.Email()`
- DateTime: `_faker.Date.Past()`
- Bool: `_faker.Random.Bool(0.8f)` (80% true)
### BuildMany-Methode
- Signatur: `public IEnumerable<T> BuildMany(int count)`
- Ruft Build() count-mal in einer Schleife auf
- Gibt IEnumerable<T> zurück
Best Practices für Bogus und Testdata-Builder
Bei der Kombination von Bogus mit Testdata-Buildern haben sich folgende Practices bewährt:
- Performance: Der statische Faker vermeidet unnötige Objektinstanzierungen. Ab mehr als 100 Objekten macht sich das bemerkbar.
- Locale-Konsistenz: Die deutsche Locale ("de") erzeugt deutschsprachige Namen und Adressen. Für internationale Projekte lässt sich dies anpassen.
- Wahrscheinlichkeiten: Bei Boolean-Werten wie IsPublished ist es sinnvoll, keine 50:50-Verteilung zu verwenden, sondern realistische Wahrscheinlichkeiten (_faker.Random.Bool(0.8f)für 80% publizierte Posts).
- With-Methoden bleiben wichtig: Auch mit Bogus sollten Tests nur die relevanten Eigenschaften explizit setzen. Die Kombination aus zufälligen Defaults und gezielten Overrides macht Tests robust und aussagekräftig.
- Deterministische Tests, wenn nötig: Für manche Tests benötigt man reproduzierbare Zufallswerte. Bogus unterstützt hierfür Seeds: new Faker("de").UseSeed(1234).
Fazit
Die Kombination aus Testdata-Buildern und Bogus bietet das Beste aus beiden Welten: Die Struktur und Wartbarkeit des Builder-Patterns trifft auf die Realitätsnähe und Effizienz automatisch generierter Testdaten. In Verbindung mit KI-Assistenten entsteht ein produktiver Workflow, der sowohl bei einfachen Unit-Tests als auch bei umfangreichen Integrationstests überzeugt.
Während einfache Builder mit statischen Werten für Unit-Tests oft ausreichen, zeigt Bogus seine Stärken bei Integrationstests, Performance-Tests und überall dort, wo größere Datenmengen benötigt werden. Die Investition in diese Infrastruktur zahlt sich langfristig aus: Tests werden robuster, Randfall-Fehler werden früher erkannt, und die Testsuite bleibt trotz wachsender Komplexität wartbar.
Die hier gezeigte Grundstruktur lässt sich auf beliebige Domänenmodelle übertragen. Ob E-Commerce, CRM oder IoT-Anwendung – das Muster bleibt gleich: Builder für Struktur, Bogus für Realismus, KI für Effizienz.
[1] Alexander Rampp, Bob, der Testdaten-Baumeister, dotnetpro 10/11/2025, Seite 104 ff.