Zur sinnvollen Verifizierung der Korrektheit von Software gehören aussagekräftige Tests. Eine immer wiederkehrende Tätigkeit ist das Schreiben von Quellcode, der Testdaten erzeugt oder beschreibt. Dies kann je nach Umfang der Daten mühsam sein und den Entwicklungsprozess ausbremsen und im schlimmsten Fall dazu führen, dass auf Tests ganz verzichtet wird. Eine elegante Möglichkeit, Testdaten einfach und schnell zu erzeugen, sind Dummy-Factories.

Test-Daten

Für das Aufrufen von Methoden aus Tests heraus, müssen diese zumeist mit definierten Eingabeparametern befüttert werden. Je weiter unten in der Test-Pyramide wir uns befinden, desto synthetischer werden diese Daten sein. Die Struktur der Daten kann von einfachen primitiven Datentype wie Strings, Boolean- oder Zahlwerten bis hin zu komplexen Objektstrukturen reichen.

Sehen wir uns eine imaginäre E-Commerce-Software mit folgenden Daten-Klassen an. Die Beispiele sind in C# gehalten, lassen sich aber auf andere Programmiersprachen übertragen.

public record Address(Guid Id, string Street, string City,
  string State, string ZipCode);

public record Customer(Guid Id, string FirstName, string LastName,
  Address Address, string Email, bool Active);

public record Product(string Sku, string Name,
  string Description, decimal Price);

public record Order(string OrderNumber, DateTime OrderDate,
  Customer Customer, IEnumerable<OrderItem> Items);

public record OrderItem(Product Product, int Quantity, decimal Price);

Für eine Methode OrderService.PlaceOrder(..) könnte ein Unit-Test etwa so beginnen:

[Test]
public void Should_Place_Order()
{
    // Arrange
    var address = new Address(Guid.NewGuid(), "123 Main St",
      "Anytown", "TX", "12345");
    var customer = new Customer(Guid.NewGuid(), "John", "Doe", address,
      "john.doe@example.com", true);

    var product1 = new Product("P-001", "Product 1", "Product 1 Description", 9.99m);
    var product2 = new Product("P-002", "Product 2", "Product 2 Description", 19.99m);

    var orderItems = new[]
    {
        new OrderItem(product1, 1, product1.Price),
        new OrderItem(product2, 2, product2.Price)
    };
    var order = new Order("O-001", DateTime.UtcNow, customer, orderItems);

    // Act
    var sut = new OrderService();
    sut.PlaceOrder(order);

    // Assert
    //...
}

Bis wir aber an den Punkt kommen, unsere zu testende Methode mit den richtigen Parametern aufrufen zu können, müssen wir einige Zeilen an Setup-Code schreiben, nur um eine gültige Order zu erzeugen. Eine Order enthält einen Customer mit einer Address und eine Menge an OrderItems mit zugehörigen Products.

Dieser Code wird nicht nur sehr schnell redundant, sondern auch fehleranfällig. Wenn wir uns nun vorstellen, dass wir für jede Test-Methode eine neue Order erzeugen müssen, wird schnell klar, dass wir hier eine Menge Zeit verlieren können.

Ein weiterer Nachteil: Wenn sich an unseren Daten-Klassen etwas ändern, etwa weil ein Customer nun auch eine Telefonnummer haben muss, und wir Nicht-optionale Parameter hinzufügen, müssen wir die Constructor-Aufrufen in allen Testmethoden, die diese Daten-Instanzen erzeugen, anpassen.

Dummy-Factories

Abhilfe schafft das Anlegen von Dummy-Factories. Dies sind Klassen und Methoden, die uns nach einmaligem Schreiben in allen Tests zur Verfügung stehen und das Erzeugen von Testdaten erleichtern. Dazu wird für jede Daten-Klasse eine Dummy-Methode angelegt und wo möglich mit Default-Parametern ausgestattet. Diese können bei Bedarf in den Testmethoden für den jeweiligen Anwendungsfall passend überschrieben werden. Für verschachtelte Datenstrukturen können etwa Nullable reference types verwendet werden, welche bei Nicht-vorhandensein wiederum durch Dummies ersetzt werden.

public static class Dummies
{
    public static Address Address(
        Guid? id = null,
        string street = "123 Main St",
        string city = "Anytown",
        string state = "TX",
        string zipCode = "12345") =>
        new(id ?? Guid.NewGuid(), street, city, state, zipCode);

    public static Customer Customer(
        Guid? id = null,
        string firstName = "John",
        string lastName = "Doe",
        Address? address = null,
        string email = "john.doe@example.com",
        bool active = true) =>
        new(id ?? Guid.NewGuid(), firstName, lastName, 
          address ?? Address(), email, active);

    public static Product Product(
        string sku = "P-001",
        string name = "Product 1",
        string description = "Product 1 Description",
        decimal price = 9.99m) =>
        new(sku, name, description, price);
    
    // ...
}

Mit kleinen Hilfsmethoden kann man sich das Leben weiter erleichtern.

public static T[] Many<T>(params T[] items) => items.ToArray();

Die oben gezeigte Testmethode kann nun wie folgt aussehen:

using static Company.Webstore.Tests.Dummies;

[Test]
public void Should_Place_Order()
{
    // Arrange
    var orderItems = Many(
        OrderItem(),
        OrderItem(Product(sku: "P-002", price: 19.99m), 2));
    var order = Order(items: orderItems);

    // Act
    var sut = new OrderService();
    sut.PlaceOrder(order);

    // Assert
    //...
}

Durch das Verwenden eines using static Statements lassen sich die Dummy-Methode direkt aufrufen, ohne den Klassennamen voranstellen zu müssen.

Damit ist unser Test-Setup auf wenige relevante Zeilen reduziert.

Sollte sich nun etwas an unseren Daten-Klassen ändern, müssen wir nur noch die Dummy-Methode anpassen und die Testmethoden bleiben unberührt.

Alternativen

Zum generieren von mehr oder weniger zufälligen Test- oder Fake-Daten, existieren natürlich Libraries wie Bogus oder FsCheck. Hierbei gibt man natürlich etwas Kontrolle ab, das verwenden von Dummies ist wesentlich expliziter. Je nach Anwendungsfall kann das aber auch ein Vorteil sein und die zusätzliche Komplexität rechtfertigen.