Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 650
cedd Avatar asked Sep 28 '22 02:09

cedd


1 Answers

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
like image 174
Mark Seemann Avatar answered Dec 31 '22 22:12

Mark Seemann