Модульное тестирование цикломатически сложно, но в остальном тривиальные вычисления

Допустим, у меня есть класс калькулятора, основной функцией которого является выполнение следующего (этот код упрощен для упрощения обсуждения, пожалуйста, не комментируйте его стиль)

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;

Через этот класс есть как минимум 7 маршрутов, которые я, вероятно, должен проверить, комбинацию trackCostMultiplier и hardstandingsRequireRemediation (6 комбинаций) и условие исключения. Я также хотел бы добавить некоторые для деления на ноль и переполнения и тому подобное, если я чувствую себя заинтересованным.

Пока все хорошо, я могу легко и стильно протестировать это количество комбинаций. И на самом деле я мог бы полагать, что умножение и сложение вряд ли пойдут не так, поэтому просто проведите 3 теста для trackCostMultipler и 2 для hardstandingsRequireRemediation вместо тестирования всех возможных комбинаций.

Однако это простой случай, и логика в наших приложениях, к сожалению, циклически намного сложнее, поэтому количество тестов может возрасти до огромных.

Есть несколько способов справиться с этой сложностью

  1. Извлеките расчет trackCostMultipler для метода в том же классе

Это хорошая вещь, но она не поможет мне протестировать ее, если я не сделаю этот метод общедоступным, что является формой "Test Logic In Production". Я часто делаю это во имя прагматизма, но я хотел бы избежать, если смогу.

  1. Отложить расчет trackCostMultipler для другого класса

Это хорошо, если вычисления достаточно сложны, и я могу легко протестировать этот новый класс. Однако я только что сделал тестирование исходного класса более сложным, так как теперь я хочу передать какой-нибудь ITrackCostMultipler "Test Double", проверить, что он вызывается с правильными параметрами, и проверить, используется ли его возвращаемое значение. правильно. Когда у класса есть, скажем, десять вспомогательных калькуляторов, его юнит / интеграционный тест становится очень большим и трудным для понимания.

Я использую оба (1) и (2), и они дают мне уверенность, и они делают отладку намного быстрее. Однако есть определенные недостатки, такие как тестовая логика в производственных и неясных тестах.

Мне интересно, каков опыт тестирования цикломатически сложного кода? Есть ли способ сделать это без минусов? Я понимаю, что Test Specific Subclasses может обойти (1), но это кажется мне устаревшей техникой. Также возможно манипулировать входными данными, чтобы различные части вычисления возвращали 0 (для сложения или вычитания) или 1 (для умножения или деления), чтобы упростить тестирование, но это только помогает мне.

Спасибо

Cedd

2 ответа

Вам нужен другой уровень абстракции, чтобы упростить ваши методы, чтобы их было проще тестировать:

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;
}

Извините за код, я не знаю языка, на самом деле не имеет значения...

Если вы не хотите делать эти методы общедоступными, вы должны переместить их в отдельный класс и сделать их общедоступными. Имя класса может быть TrackCostMultiplerAlgorithm или..Logic или..Counter, или что-то в этом роде. Таким образом, вы сможете внедрить алгоритм в код более высокого уровня абстракции, если у вас будет больше разных алгоритмов. Все зависит от реального кода.

О, и не беспокойтесь о длине метода и класса, если вам действительно нужен новый метод или класс, потому что код слишком сложный, тогда создайте его! Не важно, что это будет коротко. Это также всегда облегчит понимание, потому что вы можете написать в методе, что он делает. Блок кода внутри метода только говорит нам, как это происходит...

Продолжая обсуждение от комментариев к OP, если у вас есть ссылочно-прозрачные функции, вы можете сначала протестировать каждую небольшую деталь отдельно, а затем объединить их и проверить правильность комбинации.

Поскольку составляющие функции прозрачны по ссылкам, они логически взаимозаменяемы с их возвращаемыми значениями. Теперь единственным оставшимся шагом было бы доказать, что общая функция правильно составляет отдельные функции.

Отлично подходит для тестирования на основе свойств.

В качестве примера предположим, что у вас есть две части сложного вычисления:

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

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

Обе эти функции являются детерминированными, поэтому при условии, что вы действительно хотите проверить facade Функция, которая объединяет эти две функции, вы можете определить это свойство:

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

Как и другие инфраструктуры тестирования на основе свойств, FsCheck будет генерировать множество случайно сгенерированных значений в facadeReturnsCorrectResult (100 раз по умолчанию).

Учитывая, что оба complexPart1 а также complexPart2 детерминированы, но вы не знаете, что x а также y Единственный способ пройти тест - правильно реализовать функцию:

let facade x y = 
    let intermediateResult = complexPart1 x y
    complexPart2 x intermediateResult
Другие вопросы по тегам