DEV Community

Cover image for Reduce your tests cognitive complexity with AutoFixture
Pierre Bouillon for SFEIR

Posted on

Reduce your tests cognitive complexity with AutoFixture

When unit testing your components, you may often be in a situation when you must provide several parameters but only one is relevant.

Ensuring that your test is still readable and not bloated by the setup of those variables may be quite a challenge but hopefully no more with AutoFixture, let's see how!

Setup

If you want to, you can make the same manipulations as I am doing, like a small hands on lab on AutoFixture. If not, you may skip this chapter and head directly to the case study.

The setup here is really minimal, just create a new test project. I'll be using xUnit but NUnit should be fine too.

~$ dotnet new xunit -o AutoFixtureExample
Enter fullscreen mode Exit fullscreen mode

You can now open your new project in an editor of your choice and delete the generated UnitTest1.cs file.

Case study

For us to understand why AutoFixture might be an asset in your projects, we will work on a simple case study: considering a warehouse and an order for clothes, we want to either confirm or refuse to process the order.

In a new file Warehouse.cs, let's first add our entities:

// Warehouse.cs
public record Cloth(string Name);

public record Order(Cloth cloth, int UnitsOrdered, double Discount);

public class Warehouse
{
}
Enter fullscreen mode Exit fullscreen mode

Finally, append our simple ordering validation inside the Warhouse class:

// Warehouse.cs
public bool IsValid(Order order)
{
    if (order.UnitsOrdered < 1) return false;

    // Some more checks on the stocks

    return true;
}
Enter fullscreen mode Exit fullscreen mode

Now that we have our logic, we can test it in a new WarehouseTest.cs file:

// WarehouseTest.cs
public class WarehouseTest
{
    [Fact]
    public void OrderingWithAnInvalidUnitsCount()
    {
        var order = new Order(new Cloth("sweat"), -1, 0);

        var result = new Warehouse().IsValid(order);

        Assert.False(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

You may now ensure that our test is passing by running the tests.

Some problem

Our test may pass but it may not be as well written as we would like.

Readability and intent

Let's tackle the first issue here which might be readability.

In our example, the test is fairly simple but a newcomer on the project might not know what the -1 really means, and maybe not that the kind of cloth we are creating here is irrelevant.

We may want to clarify it by naming our variables:

// WarhouseTest.cs
public class WarehouseTest
{
    [Fact]
    public void OrderingWithAnInvalidUnitsCount()
    {
-       var order = new Order(new Cloth("sweat"), -1, 0);
+       var invalidUnitsCount = -1;
+       var cloth = new Cloth("This does not matter for the test");
+       var irrelevantDiscount = 0;
+       var order = new Order(cloth, invalidUnitsCount, irrelevantDiscount);

        var result = new Warehouse().IsValid(order);

        Assert.False(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Variables that are purposely created to indicate that they does not matter are also sometimes referred as anonymous variables in AutoFixture.

It's now a bit more verbose but the intent of the test is clearer and might help someone new to grasp what the parameters are for.

Notice that strings can hold their intents (ex: "My value does not matter") but other types may not such as int, double, etc. That's why we had to name our variable holding the discount with this explicit name.

Surviving refactoring

Another issue that you may face is the classes used in your tests evolving.

Let's say that our Cloth class now also contains its marketing date, we will have to update our test in consequence:

// Warehouse.cs
- public record Cloth(string Name);
+ public record Cloth(string Name, DateTime MarketingDate);
Enter fullscreen mode Exit fullscreen mode
// WarehouseTest.cs
public class WarehouseTest
{
    [Fact]
    public void OrderingWithAnInvalidUnitsCount()
    {
        var invalidUnitsCount = -1;
-       var cloth = new Cloth("This does not matter for the test");
+       var cloth = new Cloth("This does not matter for the test", DateTime.Now);
        var irrelevantDiscount = 0;
        var order = new Order(cloth, invalidUnitsCount, irrelevantDiscount);

        var result = new Warehouse().IsValid(order);

        Assert.False(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Having this change already impacted our test even with our example that is minimal. If we had more tests or objects using Cloth we would have a lot more refactoring to do.

You may also notice that we are passing DateTime.Now here, which is yet not very readable regarding its intent.

Introducing AutoFixture

Our test is slowly getting more and more bloated with those initialization and may be even more if either of our classes evolve.

Hopefully, using AutoFixture, we can greatly simplify it.

AutoFixture is a NuGet that can generate variables that can be seen as explicitly not significant.

Having a glance at their README, it appears that it is exactly what we would need:

AutoFixture is designed to make Test-Driven Development more productive and unit tests more refactoring-safe. It does so by removing the need for hand-coding anonymous variables as part of a test's Fixture Setup phase.

Let's add AutoFixture and see what's changing !

~/AutoFixtureExample$ dotnet add package AutoFixture
Enter fullscreen mode Exit fullscreen mode
// WarehouseTest.cs
public class WarehouseTest
{
    private static readonly IFixture Fixture = new Fixture();

    [Fact]
    public void OrderingWithAnInvalidUnitsCount()
    {
        var invalidUnitsCount = -1;
        var cloth = Fixture.Create<Cloth>();
        var discount = Fixture.Create<double>();
        var order = new Order(cloth, invalidUnitsCount, discount);

        var result = new Warehouse().IsValid(order);

        Assert.False(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

That's a little bit better since now we do not have to specify which value is relevant and which one is not. However, the test is still pretty long and we can take advantage of AutoFixture's builder to create our order in an even more straightforward way:

// WarehouseTest.cs
public class WarehouseTest
{
    private static readonly IFixture Fixture = new Fixture();

    [Fact]
    public void OrderingWithAnInvalidUnitsCount()
    {
-       var invalidUnitsCount = -1;
-       var cloth = Fixture.Create<Cloth>();
-       var discount = Fixture.Create<double>();
-       var order = new Order(cloth, invalidUnitsCount, discount);
+       var order = Fixture.Build<Order>()
+           .With(order => order.UnitsOrdered, -1)
+           .Create();

        var result = new Warehouse().IsValid(order);

        Assert.False(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

We now have a test where only the variable that matters is explicitly set and that does not need to be modified if any class changes.

Take aways

Using AutoFixture, we have greatly improved our test's readability and clarify its intents while also ensuring that it will not break whenever a class's definition changes.

Of course there is much more to learn about this library, such as how to customize the objects generations, creating sequences and more and for that you can refer to their GitHub and the associated cheat sheet that can be a good starting point for using AutoFixture.

Top comments (0)