Do you isolate your logic and remove dependencies on instances of objects?

Last updated by Daniel Mackay [SSW] about 1 year ago.See history

If there are complex logic evaluations in your code, we recommend you isolate them and write unit tests for them.

Take this for example:

while ((ActiveThreads > 0 || AssociationsQueued > 0) && (IsRegistered || report.TotalTargets <= 1000 )
 && (maxNumPagesToScan == -1 || report.TotalTargets < maxNumPagesToScan) && (!CancelScan))

Figure: This complex logic evaluation can't be unit tested

Writing a unit test for this piece of logic is virtually impossible - the only time it is executed is during a scan and there are lots of other things happening at the same time, meaning the unit test will often fail and you won't be able to identify the cause anyway.

We can update this code to make it testable though.

Update the line to this:

while (!HasFinishedInitializing (ActiveThreads, AssociationsQueued, IsRegistered, 
 report.TotalTargets, maxNumPagesToScan, CancelScan))

Figure: Isolate the complex logic evaluation

We are using all the same parameters - however, now we are moving the actual logic to a separate method.

Now create the method:

private static bool HasFinishedInitializing(int ActiveThreads, int AssociationsQueued, bool IsRegistered, 
 int TotalAssociations, int MaxNumPagesToScan, bool CancelScan)
{
 return (ActiveThreads > 0 || AssociationsQueued > 0) && (IsRegistered || TotalAssociations <= 1000 )
 && (maxNumPagesToScan == -1 || TotalAssociations < maxNumPagesToScan) && (!CancelScan);		
}

Figure: Function of the complex logic evaluation

The critical thing is that everything the method needs to know is passed in, it mustn't go out and get any information for itself and mustn't rely on any other objects being instantiated. In Functional Programming this is called a "Pure Function". A good way to enforce this is to make each of your logic methods static. They have to be completely self-contained.

The other thing we can do now is actually go and simplify / expand out the logic so that it's a bit easier to digest.

public class Initializer
{
    public static bool HasFinishedInitializing(
        int ActiveThreads, 
        int AssociationsQueued, 
        bool IsRegistered,
        int TotalAssociations, 
        int MaxNumPagesToScan, 
        bool CancelScan)
    {
        // Cancel
        if (CancelScan)
            return true;


        // Only up to 1000 links if it is not a registered version
        if (!IsRegistered && TotalAssociations > 1000)
            return true;


        // Only scan up to the specified number of links
        if (MaxNumPagesToScan != -1 && TotalAssociations > MaxNumPagesToScan)
            return true;


        // Not ActiveThread and the Queue is full
        if (ActiveThreads <= 0 && AssociationsQueued <= 0)
            return true;


        return false;
    }
}

Figure: Simplify the complex logic evaluation

The big advantage now is that we can unit test this code easily in a whole range of different scenarios!

public class InitializerTests
{
    [Theory()]
    [InlineData(2, 20, false, 1200, -1, false, true)]
    [InlineData(2, 20, true, 1200, -1, false, false)]
    public void Initialization_Logic_Should_Be_Correctly_Calculated(
        int activeThreads, 
        int associationsQueued, 
        bool isRegistered, 
        int totalAssociations, 
        int maxNumPagesToScan, 
        bool cancelScan, 
        bool expected)
    {
        // Act
        var result = Initializer.HasFinishedInitializing(activeThreads, associationsQueued, isRegistered, totalAssociations, maxNumPagesToScan, cancelScan);

        // Assert
        result.Should().Be(expected, "Initialization logic check failed");
    }
}

Figure: Write a unit test for complex logic evaluation

We open source. Powered by GitHub