Search code examples
c#unit-testingdata-driven-testsparameterized-unit-test

Is there an easier way to handle unit testing a method with too many conditions?


I have a method which has a lot of conditions in it:

public bool IsLegalSomething(Order order)
{
    var item0 = order.Items.SingleOrDefault(x => x.ItemCode == "ItemCode0");
    var item1 = order.Items.SingleOrDefault(x => x.ItemCode == "ItemCode1");
    ...
    var itemN = order.Items.SingleOrDefault(x => x.ItemCode == "ItemCodeN");

    return ((item0.Status == Status.Open) && (item1.Status == Status.Closed)
         && ...
         && (itemN.Status == Status.Canceled));
}

I want to unit test this function, but there are so many conditions that the number of unit tests is crazy if you consider every combination. There are 16 conditions in that return statement, and since each condition is true/false that is 2^16 different combinations I would need to check on. Do I really need to create 2^16 different unit tests here to ensure that every condition is utilized? Mind you, this is a simple example. Some of my functions have complex conditions due to legal requirements:

return (condition0 && condition1 && (condition2 || condition3)
     && (condition4 || (condition5 && condition6)) ...)

By the math of some of my functions, the number of different combinations that the conditions can produce is millions! I looked into Data-Driven Unit Tests (DDUT), as well as Parameterized Unit Tests (PUT), but that just makes it so that the unit test is a "fill in the blanks" style. I still have to supply all of the various combinations and the expected result! For example:

// Parameterized Unit Test
[TestCase(..., Result = true)]  // Combination 0
[TestCase(..., Result = true)]  // Combination 1
[TestCase(..., Result = false)] // Combination 2
public bool GivenInput_IsLegalSomething_ReturnsValidResult(...) { }

If I use MSTest to pull in a datasource (csv for example), I still have the same problem. I have way too many combinations that give different results. Is there an alternative I just am unaware of?


Solution

  • While I agree with the comments made about refactoring your code, I think a more concise answer is warranted to explain what exactly is meant by "The design may need to be reviewed and refactored."

    Let's look at the following statement: A && B && (C || D);. Normally, you would say that you have 4 inputs @ 2 options/each, or 16 combinations. However, if you refactor things you can reduce down the complexity. This will vary depending on your business domain, so I'm going to utilize a shopping site domain as an example (we don't actually know your business domain).

    • A: Is New Order
    • B: Are All Items In Stock
    • C: Is Customer Elite Member
    • D: Is Discount Code FREESHIP Used

    The reason I chose this scenario is to demonstrate that it is possible that C/D actually references whether or not shipping should be included free of charge.

    • E: Is Shipping Free

    Now, instead of A && B && (C || D) we have A && B && E, which is 3 conditions @ 2 options/each, or 8 combinations. Of course, the composition of E should be tested as well, but C || D only has 2 options @ 2 options/each, or 4 combinations. We have reduced down the number of total combinations from 16 to 12. While that may not seem like much, the scale of this scenario was far less. If you are able to group logical conditions together and reduce things further, you could go from millions of combinations to just a few hundred, which is far more easy to maintain.

    Additionally, as a bonus, sometimes your domain logic changes in some aspects, but not in others. Imagine that you decide that Commercial clients also receive free shipping one day, instead of adding another condition to an extremely complex conditional statement, essentially doubling the number of unit tests, you would add that condition to the Is Free Shipping, which would increase the number of combinations for that smaller unit from 4 to 8, but it's better than having a function that has 16 conditions (i.e. 65536 combinations) to 17 conditions (i.e. 131072 combinations).


    At this point, unless we know and understand your exact domain, we can only make broad suggestions of redesigning your classes and methods into smaller parts. Also, while mart makes a good point using string length > 5 not needing to test for every string greater than 5 length, I do think that once you have an actual condition reduced down into true/false that combined conditions need to be tested. For example: (String Length > 5) && B && C && D && E has 5 conditions or 32 combinations. You should test all 32. What you shouldn't do is come up with 100 different ways to show that String Length > 5 is true. The reason I say to test all 32 combinations is because while the conditions can be refactored and tested, you still want to test that you are using A && B && (C || D) so you can be sure no one made a typo of A && B || (C && D). Or, in other words, proving the individual conditions does not guarantee that the combination of those conditions was correctly coded.