DEV Community

loading...

Prevent domain knowledge from sneaking into solitary tests

Jan Van Ryswyck
Owner @ Principal IT, husband, father of three, geek, enjoys running, fan boy of many things but nothing in particular.
Originally published at principal-it.eu on ・3 min read

Previously we discussed why solitary tests should be easy to read. Sometimes, the readability of solitary tests is affected by those developers who overcomplicate or overengineer things. Well intentioned no doubt, but in the end quite harmful nonetheless. Complex solitary tests can cause some serious headaches for other members of the team.

One example of this is an issue that I see popping up from time to time. It’s the case where domain logic sneaks into the implementation of solitary tests. This seems to occur most often in solitary tests that exercise algorithms or business logic in domain objects.

Let’s have a look at an example to see this in action.

public class SolarPanelInstallation
{
    public IEnumerable<SolarPanel> SolarPanels { get; }

    public SolarPanelInstallation(IEnumerable<SolarPanel> solarPanels)
    {
        SolarPanels = solarPanels;
    }

    public Watts CalculateTheoreticalCapacity()
    {
        return SolarPanels.Aggregate(Watts.Of(0), (accumulator, solarPanel) 
            => accumulator + solarPanel.Capacity);
    }
}

public class SolarPanel
{
    public Watts Capacity { get; }

    public SolarPanel(Watts capacity)
    {
        Capacity = capacity;
    }
}

public readonly struct Watts
{
    public int Value { get; }

    private Watts(int value)
    {
        Value = value;
    }

    public static Watts Of(int value)
    {
        return new Watts(value);
    }

    public static Watts operator +(Watts a, Watts b)
    {
        return Of(a.Value + b.Value);
    }

    public override string ToString()
    {
        return $"{Value} Watts";
    }
}

Enter fullscreen mode Exit fullscreen mode

Here we’ve entered the realm of generating green energy using solar panels. A solar panel installation can be comprised of one or multiple solar panels. Each solar panel has its own capacity which is expressed in watts. The SolarPanelInstallation class provides a method that calculates the theoretical capacity of the entire installation.

Let’s have a look at the test code.

[Specification]
public class When_calculating_the_theoretical_capacity_of_a_solar_panels_installation
{
    [Establish]
    public void Context()
    {
        var solarPanels = new[]
        {
            new SolarPanel(Watts.Of(368)), 
            new SolarPanel(Watts.Of(368)), 
            new SolarPanel(Watts.Of(278)) 
        };

        _sut = new SolarPanelInstallation(solarPanels);
    }

    [Because]
    public void Of()
    {
        _theoreticalCapacity = _sut.CalculateTheoreticalCapacity();
    }    

    [Observation]
    public void Then_it_should_yield_the_total_capacity_of_all_solar_panels_of_the_installation()
    {
        var expectedCapacity = _sut.SolarPanels
            .Select(solarPanel => solarPanel.Capacity.Value)
            .Sum();

        _theoreticalCapacity.Should_be_equal_to(Watts.Of(expectedCapacity));
    }

    private SolarPanelInstallation _sut;
    private Watts _theoreticalCapacity;
}

Enter fullscreen mode Exit fullscreen mode

Notice how the value of the expectedCapacity variable is being calculated. This is very similar to the calculation in the CalculateTheoreticalCapacity method of the SolarPanelInstallation class. Although the way their respective implementation calculates the capacity is slightly different, we can conclude that in this example the test code contains the same knowledge as the production code. Sometimes, I even encounter tests where the developer just “borrowed” from the production code directly.

This is also a nice example where state verification tests are too tightly coupled to the production code. So when the implementation of the CalculateTheoreticalCapacity method is refactored, chances are quite high that the test code needs to be modified as well.

It’s also more difficult to read this test and figure out what the expected value should be. For an easy example like this it doesn’t require that much additional brain cycles. However, with more complex algorithms or business logic, developers often execute the test in debug mode just to figure out what the expected value should be. How’s that for readability?

Let’s have a look at an improved version of this test.

[Specification]
public class When_calculating_the_theoretical_capacity_of_a_solar_panels_installation
{
    [Establish]
    public void Context()
    {
        var solarPanels = new[]
        {
            new SolarPanel(Watts.Of(368)), 
            new SolarPanel(Watts.Of(368)), 
            new SolarPanel(Watts.Of(278)) 
        };

        _sut = new SolarPanelInstallation(solarPanels);
    }

    [Because]
    public void Of()
    {
        _theoreticalCapacity = _sut.CalculateTheoreticalCapacity();
    }    

    [Observation]
    public void Then_it_should_yield_the_total_capacity_of_all_solar_panels_of_the_installation()
    {
        _theoreticalCapacity.Should_be_equal_to(Watts.Of(1014));
    }

    private SolarPanelInstallation _sut;
    private Watts _theoreticalCapacity;
}

Enter fullscreen mode Exit fullscreen mode

Here we just provided the value that we expect to be the result of the calculation. That’s it! No more duplicate domain knowledge, no more tight coupling of the test and no more debugging. The expected value is just right there. Simplicity can be a beautiful thing.

Domain knowledge that sneaks into your tests is something to be avoided. Be very mindful about this. Don’t use the Subject Under Test itself for determining the outcome of a test.

Discussion (0)