Сложность мышления о свойствах для FsCheck

Мне удалось получить xUnit, работая над моей небольшой сборкой образцов. Теперь я хочу посмотреть, могу ли я grok FsCheck тоже. Моя проблема в том, что я в тупике, когда дело доходит до определения свойств теста для моих функций.

Может быть, у меня просто нет хорошего набора пробных функций, но какие бы хорошие тестовые свойства для этих функций, например?

//transforms [1;2;3;4] into [(1,2);(3,4)]
pairs : 'a list -> ('a * 'a) list      //'

//splits list into list of lists when predicate returns 
//  true for adjacent elements
splitOn : ('a -> 'a -> bool) -> 'a list -> 'a list list

//returns true if snd is bigger
sndBigger : ('a * 'a) -> bool (requires comparison)

Ответ 1

Уже есть много конкретных ответов, поэтому я попытаюсь дать некоторые общие ответы, которые могут дать вам некоторые идеи.

  • Индуктивные свойства для рекурсивных функций. Для простых функций это, вероятно, повторяет реализацию рекурсии. Однако сохраните его просто: хотя фактическая реализация чаще всего не развивается (например, она становится хвостовой рекурсивной, вы добавляете memoization,...) сохраняете свойство просто. Комбинатор свойств == > обычно пригодится здесь. Хорошим примером может служить функция ваших пар.
  • Свойства, которые содержат несколько функций в модуле или типе. Обычно это происходит при проверке типов абстрактных данных. Например: добавление элемента в массив означает, что массив содержит этот элемент. Это проверяет соответствие файлов Array.add и Array.contains.
  • Круговые поездки: это полезно для конверсий (например, синтаксический анализ, сериализация) - генерирует произвольное представление, сериализует его, десериализует, проверяет, что оно равно оригиналу. Вы можете сделать это с помощью splitOn и concat.
  • Общие свойства как проверки работоспособности. Ищите общеизвестные свойства, которые могут иметь место - такие вещи, как коммутативность, ассоциативность, идемпотентность (применение чего-то дважды не изменяет результат), рефлексивность и т.д. Идея здесь состоит в том, чтобы немного использовать функцию - посмотрите, действительно ли она что-то действительно странно.

Как общий совет, постарайтесь не делать слишком большой сделки из этого. Для sndBigger хорошим свойством будет:

пусть `` должен возвращать true тогда и только тогда, когда snd больше `` (a: int) (b: int) =   sndBigger (a, b) = b > a

И это, вероятно, именно реализация. Не беспокойтесь об этом - иногда простой, старомодный unit test - это именно то, что вам нужно. Никакой вины не требуется!:)

Возможно эта ссылка (команда Pex) также дает некоторые идеи.

Ответ 2

Я начну с sndBigger - это очень простая функция, но вы можете написать некоторые свойства, которые должны быть об этом. Например, что происходит, когда вы меняете значения в кортеже:

// Reversing values of the tuple negates the result
let swap (a, b) = (b, a)
let prop_sndBiggerSwap x = 
  sndBigger x = not (sndBigger (swap x))

// If two elements of the tuple are same, it should give 'false'
let prop_sndBiggerEq a = 
  sndBigger (a, a) = false

EDIT: Это правило prop_sndBiggerSwap не всегда выполняется (см. комментарий kvb). Однако должно быть правильно:

// Reversing values of the tuple negates the result
let prop_sndBiggerSwap a b = 
  if a <> b then 
    let x = (a, b)
    sndBigger x = not (sndBigger (swap x))

Что касается функции pairs, kvb уже опубликовал некоторые хорошие идеи. Кроме того, вы можете проверить, что преобразование преобразованного списка в список элементов возвращает исходный список (вам нужно будет обрабатывать случай, когда список ввода нечетный - в зависимости от того, что должна делать функция pairs в этом случае ):

let prop_pairsEq (x:_ list) = 
  if (x.Length%2 = 0) then
    x |> pairs |> List.collect (fun (a, b) -> [a; b]) = x
  else true

Для splitOn мы можем протестировать аналогичную вещь - если вы соедините все возвращенные списки, она должна предоставить исходный список (это не проверяет поведение расщепления, но это хорошо, с чего начать - минимально гарантирует, что никакие элементы не будут потеряны).

let prop_splitOnEq f x = 
  x |> splitOn f |> List.concat = x

Я не уверен, что FsCheck может справиться с этим, хотя (!), потому что свойство принимает функцию как аргумент (так что ему нужно будет генерировать "случайные функции" ). Если это не сработает, вам нужно предоставить несколько более конкретных свойств с помощью рукописной функции f. Затем выполнение проверки, что f возвращает true для всех смежных пар в разделенных списках (как предполагает kvb), на самом деле не так сложно:

let prop_splitOnAdjacentTrue f x = 
  x |> splitOn f 
    |> List.forall (fun l -> 
         l |> Seq.pairwise 
           |> Seq.forall (fun (a, b) -> f a b))

Вероятно, последнее, что вы могли проверить, это то, что f возвращает false, когда вы даете ему последний элемент из одного списка и первый элемент из следующего списка. Ниже приведено неполное, но оно показывает путь:

let prop_splitOnOtherFalse f x = 
  x |> splitOn f
    |> Seq.pairwise 
    |> Seq.forall (fun (a, b) -> lastElement a = firstElement b)

В последнем примере также показано, что вы должны проверить, может ли функция splitOn возвращать пустой список как часть возвращаемого списка результатов (потому что в этом случае вы не смогли найти первый/последний элемент).

Ответ 3

Для некоторого кода (например, sndBigger) реализация настолько проста, что любое свойство будет как минимум сложнее исходного кода, поэтому тестирование через FsCheck может не иметь смысла. Однако для других двух функций здесь есть некоторые вещи, которые вы могли бы проверить:

  • pairs
    • Что ожидалось, когда исходная длина не делится на две? Вы можете проверить, выбрали ли исключение, если это правильное поведение.
    • List.map fst (pairs x) = evenEntries x и List.map snd (pairs x) = oddEntries x для простых функций evenEntries и oddEntries, которые вы можете написать.
  • splitOn
    • Если я понимаю ваше описание того, как функция должна работать, тогда вы можете проверить такие условия, как "Для каждого списка в результате splitOn f l никакие две последовательные записи не удовлетворяют f" и "Принимать списки (l1,l2) из splitOn f l попарно, f (last l1) (first l2) выполняется". К сожалению, логика здесь, вероятно, будет сопоставима по сложности с самой реализацией.