Single Exit Pattern and Test Coverage

While helping mentor a teammate on some unit testing practices, I came across a wrinkle about how the code coverage metric can be misleading when your program flow is structured in a particular way.

Caveat: The code and unit tests are structured to show how code coverage is indicated; I realize there are different/better ways to structure the code and test that code.

Single exit

Perhaps because it was a learned style from other projects or languages, I’ve noticed in the project I currently work on that we use the single exit pattern.

The single exit pattern involves having a single return statement in a method. Here’s what it looks like:

public string GetOrderSizeDescriptor(int quantity)
{
    var size = "Unknown";

    if (quantity > 0 && quantity < 100) 
        size = "Small"; 
    else if (quantity >= 100 && quantity < 200) 
        size = "Medium"; 
    else if (quantity >= 200) 
        size = "Large";

    return size;
}

The logic seems pretty straightforward, so let’s write three happy-path unit tests:

[TestMethod]
public void When_the_quantity_is_between_1_and_99_then_the_order_is_small()
{
    for (var i = 1; i <= 99; i++)
        _system.GetOrderSizeDescriptor(i).Should().Be("Small");
}

[TestMethod]
public void When_the_quantity_is_between_100_and_199_then_the_order_is_medium()
{
    for (var i = 100; i <= 199; i++)
        _system.GetOrderSizeDescriptor(i).Should().Be("Medium");
}

[TestMethod]
public void When_the_quantity_is_200_or_bigger_then_the_order_is_large()
{
    _system.GetOrderSizeDescriptor(200).Should().Be("Large");
    _system.GetOrderSizeDescriptor(250).Should().Be("Large");
    _system.GetOrderSizeDescriptor(250000).Should().Be("Large");
}

Using the Visual Studio code coverage tools (assuming your edition of Visual Studio supports this), let’s look at the coverage:

single-exit-code-coverage

Note: If you tried this at home and saw yellow (i.e., partially covered lines), you’ll need to run the code coverage on the Release configuration. (See this post for an explanation.)

Nice — our method is 100% covered! What about if the quantity is zero or negative? Our method should return “Unknown” but we don’t have any tests for that. We get the complete code coverage because our tests (1) executed the code for assigning “Unknown”, (2) returned the variable.

Multiple exit

To highlight the difference in code coverage, let’s rewrite the code under test to return as soon as information is available for it to do so:

public string GetOrderSizeDescriptor(int quantity)
{
    if (quantity > 0 && quantity < 100) 
        return "Small"; 
    else if (quantity >= 100 && quantity < 
        return "Medium"; 
    else if (quantity >= 200) 
        return "Large";

    return "Unknown";
}

multiple-exit-code-coverage

We can now clearly see we’re missing something to cover the “Unknown” case.

Wrapping Up

Although both versions are logically correct, your code coverage tools could lead you astray if you lean on them to make sure all the edge cases are covered. In general, I prefer the multiple exit approach so I can help the tools help me.

For an interesting discussion on this style of programming, see this Programmers Stack Exchange post entitled Where did the notion of “one return only” come from?