简体   繁体   中英

Unit testing cyclomatically complicated but otherwise trivial calculations

Let's say I have a calculator class who primary function is to do the following (this code is simplified to make the discussion easier, please don't comment on the style of it)

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;

There are at least 7 routes through this class I should probably test, the combination of trackCostMultiplier and hardstandingsRequireRemediation (6 combinations) and the exception condition. I might also want to add some for divide by zero and overflow and suchlike if I was feeling keen.

So far so good, I can test this number of combinations easily and stylishly. And actually I might trust that multiplication and addition are unlikely to go wrong, and so just have 3 tests for trackCostMultipler and 2 for hardstandingsRequireRemediation, instead of testing all possible combinations.

However, this is a simple case, and the logic in our apps is unfortunately cyclomatically much more complicated than this, so the number of tests could grow huge.

There are some ways to tackle this complexity

  1. Extract the trackCostMultipler calculation to a method in the same class

This is a good thing to do, but it doesn't help me test it unless I make this method public, which is a form of "Test Logic In Production". I often do this in the name of pragmatism, but I would like to avoid if I can.

  1. Defer the trackCostMultipler calculation to a different class

This seems like a good thing to do if the calculation is sufficiently complex, and I can test this new class easily. However I have just made the testing of the original class more complicated, as I will now want to pass in a ITrackCostMultipler "Test Double" of some sort, check that it gets called with the right parameters, and check that its return value is used correctly. When a class has, say, ten sub calculators, its unit / integration test becomes very large and difficult to understand.

I use both (1) and (2), and they give me confidence and they make debugging a lot quicker. However there are definitely downsides, such as Test Logic in Production and Obscure Tests.

I am wondering what others experiences of testing cyclomatically complicated code are? Is there a way of doing this without the downsides? I realise that Test Specific Subclasses can work around (1), but this seems like a legacy technique to me. It is also possible to manipulate the inputs so that various parts of the calculation return 0 (for addition or subtraction) or 1 (for multiplication or division) to make testing easier, but this only gets me so far.

Thanks

Cedd

Continuing the discussion from the comments to the OP, if you have referentially transparent functions , you can first test each small part by itself, and then combine them and test that the combination is correct.

Since constituent functions are referentially transparent, they are logically interchangeable with their return values. Now the only remaining step would be to prove that the overall function correctly composes the individual functions.

The is a great fit for property-based testing .

As an example, assume that you have two parts of a complex calculation:

module MyCalculations =
    let complexPart1 x y = x + y // Imagine it's more complex

    let complexPart2 x y = x - y // Imagine it's more complex

Both of these functions are deterministic, so assuming that you really want to test a facade function that composes 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 x y

    let expected = (x, y) ||> complexPart1 |> complexPart2 x
    expected =! actual

Like other property-based testing frameworks, FsCheck will throw lots of randomly generated values at facadeReturnsCorrectResult (100 times, by default).

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

let facade x y = 
    let intermediateResult = complexPart1 x y
    complexPart2 x intermediateResult

You need another abstraction level to make your methods simpler, so it will be 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, does not really matter...

If you don't want to make these methods public, then you have to move them to a separate class, and make them public there. The class name could be TrackCostMultiplerAlgorithm or ..Logic or ..Counter, or something like that. So you will be able to inject the algorithm into the higher abstraction level code if you'll have more different algorithms. Everything depends on the actual code.

Ohh and don't worry about the method and class lengths, if you really need a new method or class, because the code is too complex, then create one! Does not matter that it will be short. It will be always ease understanding as well, because you can write into the method name what it does. The code block inside the method only tells us how it does...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM