Как провести тестирование единиц с неопределенностями?

У нас есть несколько различных алгоритмов оптимизации, которые дают разные результаты для каждого прогона. Например, целью оптимизации может быть поиск минимума функции, где 0 - глобальные минимумы. Прогоны оптимизации возвращают данные следующим образом:

[0.1, 0.1321, 0.0921, 0.012, 0.4]

Это довольно близко к глобальным минимумам, так что это нормально. Наш первый подход состоял в том, чтобы просто выбрать пороговое значение и позволить unit test выйти из строя, если результат был слишком высоким. К сожалению, это не работает: результаты, похоже, имеют распределение gauss, поэтому, хотя и маловероятно, время от времени тест не удался, даже если алгоритм все еще прекрасен, и нам просто не повезло.

Итак, как я могу проверить это правильно? Я думаю, что здесь нужно немного статистики. Также важно, чтобы тесты все еще были быстрыми, просто позволяя тесту пройти несколько 100 раз, а затем принять среднее значение будет слишком медленным.

Вот некоторые дополнительные пояснения:

  • Например, у меня есть алгоритм, который соответствует кругу в набор точек. Это очень быстро, но не всегда дает тот же результат. Я хочу написать unit test, чтобы гарантировать, что в большинстве случаев это достаточно хорошо.

  • К сожалению, я не могу выбрать фиксированное семя для генератора случайных чисел, потому что я не хочу проверять, дает ли алгоритм тот же результат, что и раньше, но я хочу проверить что-то вроде "с 90% уверенностью" получить результат с 0,1 или лучше ".

Ответы

Ответ 1

Похоже, ваш оптимизатор нуждается в двух видах тестирования:

  • проверка общей эффективности алгоритма
  • проверка целостности вашей реализации алгоритма

Поскольку алгоритм включает рандомизацию, (1) трудно провести единичный тест. Любое испытание случайного процесса не даст определенной доли времени. Вам нужно знать некоторые статистические данные, чтобы понять, как часто это может потерпеть неудачу. Есть способы обменяться между тем, насколько строго ваш тест и как часто он терпит неудачу.

Но есть способы написать модульные тесты для (2). Например, вы могли бы reset использовать семя для определенного значения перед запуском модульных тестов. Тогда выход детерминирован. Это не позволит вам оценить среднюю эффективность алгоритма, но это для (1). Такой тест послужил бы проводкой отключения: если кто-то ввел ошибку в код во время обслуживания, детерминированный unit test мог бы уловить ошибку.

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

Обновление. Я написал главу об этой проблеме в книге Beautiful Testing. См. Главу 10: Тестирование генератора случайных чисел.

Ответ 2

A unit test никогда не должно иметь неизвестного состояния pass/fail. Если ваш алгоритм возвращает разные значения при многократном запуске с одинаковыми входами, вы, вероятно, делаете что-то в своем алгоритме.

Я бы взял каждый из 5 алгоритмов оптимизации и протестировал их, чтобы удостовериться, что с учетом набора входов x вы каждый раз получаете оптимизированное значение y.

РЕДАКТИРОВАТЬ. Чтобы обратиться к случайным компонентам вашей системы, вы можете ввести возможность передать семя для генератора случайных чисел, который будет использоваться, или вы можете использовать насмешливую библиотеку (ala RhinoMocks), чтобы заставить его использовать определенное число, когда RNG запрашивается случайное число.

Ответ 3

У ваших алгоритмов, вероятно, есть случайный компонент. Подведите его под контроль.

Вы можете либо

  • Разрешить вызывающему абоненту выбирать семя для генератора случайных чисел. Затем в тестах используйте твердое семя.
  • Предоставить вызывающему абоненту генератор случайных чисел. Затем в тестах используйте генератор случайных чисел.

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

При использовании алгоритмов единичного тестирования вы должны проверить, правильно ли вы реализовали алгоритм. Не делает ли алгоритм то, что он должен делать. Единичные тесты не должны обрабатывать код под тестом как черный ящик.

Вы можете захотеть иметь отдельный тест "производительности", чтобы сравнить, как работают различные алгоритмы (и работают ли они на самом деле), но ваши модульные тесты действительно предназначены для тестирования вашей реализации алгоритма.

Например, при реализации Foo-Bar-Baz Optimization Algorithm (TM) вы могли случайно записать x: = x/2 вместо x: = x/3. Это может означать, что алгоритм работает медленнее, но все же находит тот же алгоритм. Чтобы найти такую ​​ошибку, вам понадобится "белый ящик".

Edit:

К сожалению, я не могу выбрать фиксированное семя для генератора случайных чисел, потому что я не хочу проверять, дает ли алгоритм тот же результат, что и раньше, но я хочу проверить что-то вроде "С уверенностью 90% я получаю результат с 0,1 или лучше".

Я не вижу никакого способа сделать тест, который автоматически проверяется и стохастичен. Особенно, если вы хотите иметь шанс отличить реальные ошибки от статистического шума.

Если вы хотите протестировать "с 90% уверенностью, я получаю результат с 0,1 или лучше", я бы предложил что-то вроде:

double expectedResult = ...;
double resultMargin = 0.1;
int successes = 0;
for(int i=0;i<100;i++){
  int randomSeed = i;
  double result = optimizer.Optimize(randomSeed);
  if(Math.Abs(result, expectedResult)<resultMargin)
    successes++; 
}
Assert.GreaterThan(90, successes);

(Обратите внимание, что этот тест является детерминированным).

Ответ 4

Пусть тесты выполняются, и если какой-либо из них терпит неудачу, повторите только эти тесты 50 раз и посмотрите, какая доля времени они терпят неудачу. (Конечно, автоматическим способом.)

Ответ 5

Я бы предположил, что вместо того, чтобы выполнить ваш тест с кодом, генерирующим гауссовский дистрибутив, вы создаете алгоритм типа Монте-Карло, который много раз запускает метод, а затем проверяет общее распределение результатов с использованием соответствующей модели распределения. Например, если это средний показатель, вы сможете протестировать его против твердого порога. Если он более сложный, вам нужно создать код, который моделирует соответствующий дистрибутив (например, do values ​​< x составляют y% от моих результатов).

Помните, что вы не тестируете генератор чисел, вы тестируете Unit, который генерирует значения!

Ответ 6

Спасибо за все ответы, я сейчас делаю это:

  • Проведите тест 5 раз и возьмите медианный результат.
  • Если медианный результат ниже определенного порога, тест завершается успешно.
  • Если порог не работает, повторите тест до тех пор, пока не будет достигнуто пороговое значение (тест будет успешным) ИЛИ пока я не выполнил столько итераций (около 100 или около того), что я могу быть уверен, что медиана не будет ниже порога больше.

Таким образом, всякий раз, когда тест выглядит так, как будто он терпит неудачу, он пересчитывается так часто, пока он не уверен, что он действительно потерпел неудачу.

Кажется, что это работает, но я не совсем удовлетворен, потому что я только тестирую медианный результат.

Ответ 7

И jUnit, и NUnit могут утверждать типы данных с плавающей точкой с допускным/дельта-значением. То есть вы проверяете, соответствует ли результат правильному значению, дайте или возьмите некоторое десятичное число. В вашем случае правильное значение, которое вы хотите проверить, равно 0, с допуском 0,5, если вы хотите, чтобы все значения в данном выходе проходили (или 0,20 с допуском +/- 0,20).

Из-за случайного характера ваших результатов вы можете захотеть unit test части алгоритма, чтобы убедиться, что он действительно делает то, что он должен.