Unit testing is cyclically complex, but otherwise trivial calculations

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.

  1. 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

+5
source share
2 answers

Continuing the discussion of OP comments, if you have link-transparent functions , you can first test each small part yourself, and then combine them and verify that the combination is correct.

Because compound functions are referentially transparent, they are logically interchangeable with their return values. Now the only remaining step would be to prove that the common function correctly constitutes the individual functions.

This is great for a property based test .

As an example, suppose you have two parts to a complex calculation:

 module MyCalculations = let complexPart1 xy = x + y // Imagine it more complex let complexPart2 xy = x - y // Imagine it more complex 

Both of these functions are deterministic, so assuming that you really want to test the facade function that combines these two functions, you can define this property:

 open FsCheck.Xunit open Swensen.Unquote open MyCalculations [<Property>] let facadeReturnsCorrectResult (x : int) (y : int) = let actual = facade xy let expected = (x, y) ||> complexPart1 |> complexPart2 x expected =! actual 

Like other property-based testing frameworks, FsCheck will return many randomly generated values ​​in facadeReturnsCorrectResult (100 times, by default).

Given that both complexPart1 and complexPart2 are deterministic, but you don't know what x and y , the only way to pass the test is to correctly implement the function:

 let facade xy = let intermediateResult = complexPart1 xy complexPart2 x intermediateResult 
+2
source

You need another level of abstraction to simplify your methods, so it’s easier to test them:

 doStuff(trackConstructionType, referenceTrackWidth){ ... trackCostMultipler = countTrackCostMultipler(trackConstructionType) countPilingCostPerArea = countPilingCostPerArea(referenceTrackWidth, trackCostMultipler) ... } countTrackCostMultipler(trackConstructionType){ 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()); return trackCostMultipler; } countPilingCostPerArea(referenceTrackWidth, trackCostMultipler){ return TrackCostPerMeter / referenceTrackWidth * trackCostMultipler; } 

Sorry for the code, I don't know the language, it really doesn't matter ...

If you do not want to publish these methods, you need to move them to a separate class and make them public. The class name may be TrackCostMultiplerAlgorithm or..Logic or..Counter, or something like that. This way you can embed the algorithm in a higher code of the abstraction level if you have more different algorithms. It all depends on the actual code.

Oh, and don't worry about the method and class lengths, if you really need a new method or class because the code is too complicated, then create one! It does not matter that it will be short. It will also be easy to understand and understand, because you can write the name of the method that it does. The code block inside the method tells us how it does it ...

+2
source

All Articles