Skip to the content.

Simplify.Your(Tests).With(Fluent.Syntax)

Learn how to write short, simple and maintainable automated software tests with a fluent interface/builder pattern.

See the .NET sample projects on this GitHub repo to see fluent syntax in action.

Example

Classic:

[Fact]
public void FindRoomForFriendsMeetup_WhenOnlyOneLargestRoomExist_ThenReturnLargestRoom2()
{
    // Arrange
    House house = new House
    {
        Floors = new[]
        {
            new Floor { Level = 1, Rooms = new[]
            {
                new Room("Kitchen", size: 10, roomNr:2, numberOfWallSockets:4, numberOfWaterSupplies:2, color:"Black", renovatedDate:new DateTime(2009, 06, 01)),
                new Room("Living Room", size: 40, roomNr:1, numberOfWallSockets:4, numberOfWaterSupplies:0, color:"White", renovatedDate:new DateTime(2020, 06, 01))
            }.ToList()},
            new Floor { Level = 2, Rooms = new[]
            {
                new Room("Bathroom", size: 5, roomNr:2, numberOfWallSockets:2, numberOfWaterSupplies:3, color:"Blue", renovatedDate:new DateTime(2012, 06, 01)),
                new Room("Bedroom", size: 10, roomNr:23, numberOfWallSockets:2, numberOfWaterSupplies:0, color:"Green", renovatedDate:new DateTime(2014, 06, 01))
            }.ToList()},
        }.ToList(),
        Garage = new Garage(),
        Pool = new Pool(),
        Garden = new Garden()
    };

    // Act
    (int level, string roomName) = new RoomFinder(house).FindRoomForFriendsMeetup();

    // Assert
    level.Should().Be(1);
    roomName.Should().Be("Living Room");
}

With Fluent Syntax:

[Fact]
public void FindRoomForFriendsMeetup_WhenOnlyOneLargestRoomExist_ThenReturnLargestRoom()
{
    // Arrange
    House house = TestHouse.Create().WithFloors(
            TestFloor.Create(level: 1).WithRoom("Kitchen", size: 10).WithRoom("Living Room", size: 40),
            TestFloor.Create(level: 2).WithRoom("Bathroom", size: 5).WithRoom("Bedroom", size: 10))
        .WithGarage().WithPool().WithGarden();

    // Act
    (int level, string roomName) = new RoomFinder(house).FindRoomForFriendsMeetup();

    // Assert
    level.Should().Be(1);
    roomName.Should().Be("Living Room");
}

What do I mean with “Fluent Syntax”?

The Principle of Chekhov’s Gun

Remove everything that has no relevance to the story. If you say in the first chapter that there is a rifle hanging on the wall, in the second or third chapter it absolutely must go off. If it’s not going to be fired, it shouldn’t be hanging there.

What it means for automated software tests:

Advantages of Fluent Syntax

How to write better tests

The following code snippets are very abstract and small for better understanding. In real situations your productive classes will have probably

I will show you the patterns based on a simple productive DTO-styled class Foo:

namespace FooProject.FeatureX;
public class Foo
{
    public Foo() { }
    public Foo(string property1, int property2, double property3)
    {
        Property1 = property1;
        Property2 = property2;
        Property3 = property3;
    }

    public string Property1 { get; set; }
    public int Property2 { get; set; }
    public double Property3 { get; set; }
    public List<Bar> Bars { get; set; } = new List<Bar>();
}

Begin with Static Methods and Classes

The very first step to improve your tests is NOT to build a fully extensible, human friendly and fancy fluent API!

I used to add a simple static helper method with required or optional parameters in the test class itself, only if more than 3 test cases use it:

private Foo CreateFoo(string property1, int property2, double property3 = 12.34) => { ... }

Then, if more than 3 test classes/fixtures need to set up the same productive class,

Continue with Static Factory Methods

When you created your new test helper classes, you can use static factory methods

Now, build your Fluent API (using Extension Methods in C#)

As soon as you have too many factory methods with too much code duplication in your test class, or when need more flexibility in your tests, then you finally could write some fluent syntax.

Usually, this is done with a Builder Pattern by setting some properties on the productive class and returning the builder instance (this). But C# has a nice feature named Extension Methods so that you can return the productive class itself and keep the test helper class static:

public static class TestFoo
{
    private static int _property1Counter = 1;

    // some properties need to be unique or random for each test
    public static string CreateParam1() => $"Property1-{_property1Counter++}";

    public static Foo Create() => new Foo().WithPropertyGroup1().WithPropertyGroup2(123);

    // split into independent groups when properties are often configured/omitted together
    public static Foo WithPropertyGroup1(this Foo foo, string property1 = null)
    {
        foo.Property1 = property1 ?? CreateParam1();
        return foo;
    }

    public static Foo WithPropertyGroup2(this Foo foo, int property2, double? optionalProperty3 = null)
    {
        foo.Property2 = property2;
        foo.Property3 = optionalProperty3 ?? TestFoo.Property3Default;
        return foo;
    }

    // shortcuts for frequently used variants
    public static Foo CreateAsVariantX() => Create().WithPropertyGroup1("X").WithPropertyGroup2(123, 12.34);
    public static Foo CreateAsVariantY() => Create().WithPropertyGroup1("Y").WithPropertyGroup2(234);
}

Or use the Builder Pattern

The Builder Pattern is an alternative to C# extension methods

Combine multiple test helpers

As productive classes can depend on other productive classes e.g. by aggregation, you can reuse other test helper classes in your actual test helper class:

public static class TestFoo
{
    public static Foo AddBar(this Foo foo, Bar? bar = null)
    {
        foo.Bars.Add(bar ?? TestBar.Create());
        return foo;
    }
}
var foo = TestFoo.Create()
    .WithPropertyGroup1("Y").WithPropertyGroup2(234)
    .AddBar(TestBar.Create().WithBarProperty("B"));

Nested Fluent Builders

You can integrate dependent builders so that the method chaining isn’t interrupted by calls to other builders:

public class TestFoo
{
    private readonly List<Bar> _bars = new();

    // continuous method chaining
    public TestBar AddBar() => new TestBar(this);

    // interrupted method chaining
    public TestFoo AddBar(Bar bar)
    {
        _bars.Add(bar);
        return this;
    }

    public Foo Build() => new Foo(_property1, _property2, _property3) { Bar = Bars };
}
var foo = new TestFoo()
    .WithPropertyGroup1("Y").WithPropertyGroup2(234)
    .AddBar().WithBarProperty("B").Add() // continuous
    .AddBar(new TestBar().WithBarProperty("B").Build()) // interrupted
    .Build();

Use Fluent Syntax as much as necessary, as little as possible