Say I have a calculator class whose main function is to do the following (this code is simplified to simplify the discussion, please do not comment on its style)
double pilingCarpetArea = (hardstandingsRequireRemediation = true) ? hardStandingPerTurbineDimensionA * hardStandingPerTurbineDimensionB * numberOfHardstandings * proportionOfHardstandingsRequiringGroundRemediationWorks : 0; double trackCostMultipler; if (trackConstructionType = TrackConstructionType.Easy) trackCostMultipler = 0.8 else if (trackConstructionType = TrackConstructionType.Normal) trackCostMultipler = 1 else if (trackConstructionType = TrackConstructionType.Hard) trackCostMultipler = 1.3 else throw new OutOfRangeException("Unknown TrackConstructionType: " + trackConstructionType.ToString()); double PilingCostPerArea = TrackCostPerMeter / referenceTrackWidth * trackCostMultipler;
In this class I have to go through at least 7 routes, which should probably be checked, a combination of trackCostMultiplier and hardstandingsRequireRemediation (6 combinations) and an exception condition. I could also add some for dividing by zero and overflow and, for example, if I felt sharp.
So far so good, I can easily and stylishly test this number of combinations. And in fact, I could trust that multiplication and addition is unlikely to go wrong, and so you just need to have 3 tests for trackCostMultipler and 2 for hardstandingsRequireRemediation instead of checking all possible combinations.
However, this is a simple case, and the logic in our applications, unfortunately, is much more complicated than the number of cycles, so the number of tests can increase.
There are several ways to solve this difficulty.
- Extract the calculation of trackCostMultipler to a method in the same class
This is a good thing, but it doesn’t help me test it unless I make this method publicly available, which is the “Test Logic In Production” form. I often do this in the name of pragmatism, but I would like to avoid it if I can.
- Postpone calculation of trackCostMultipler for another class
It seems good if the calculation is quite complicated and I can easily check out this new class. However, I just made it harder to test the source class, since now I want to pass some type of ITrackCostMultipler "Test Double", check that it is called with the correct parameters, and check that its return value is used correctly. When a class has, say, ten sub-calculators, its unit test / integration becomes very large and difficult to understand.
I use both (1) and (2) and they give me confidence and they make debugging much faster. However, there are certain disadvantages, such as Test Logic in Production and Obscure Tests.
I wonder what experiences with testing cyclically complex code? Is there any way to do this without flaws? I understand that test subclasses may work around (1), but to me it seems like an outdated technique. You can also manipulate the inputs so that the various parts of the calculation return 0 (for addition or subtraction) or 1 (for multiplication or division) to facilitate testing, but this has only brought me so far.
thanks
Cedd