Приводит ли тестирование на основе свойств к дублированию кода?

Я пытаюсь заменить некоторые старые модульные тесты тестированием на основе свойств (PBT), в частности, scala и scalatest - scalacheck, но я думаю, что проблема более общая. Упрощенная ситуация такова, если у меня есть метод, который я хочу протестировать:

 def upcaseReverse(s:String) = s.toUpperCase.reverse

Обычно я бы написал модульные тесты, например:

assertEquals("GNIRTS", upcaseReverse("string"))
assertEquals("", upcaseReverse(""))
// ... corner cases I could think of

Итак, для каждого теста я пишу ожидаемый результат, без проблем. Теперь, с PBT, это было бы так:

property("strings are reversed and upper-cased") {
 forAll { (s: String) =>
   assert ( upcaseReverse(s) == ???) //this is the problem right here!
 }
}

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

   assert ( upcaseReverse(s) == s.toUpperCase.reverse) 

То есть мне пришлось написать реализацию в тесте, чтобы убедиться, что вывод правильный. Есть ли выход из этого? Я неправильно понимаю PBT, и должен ли я вместо этого тестировать другие свойства, например:

  • "строки должны иметь ту же длину, что и оригинал"
  • "строки должны содержать все символы оригинала"
  • "строки не должны содержать символы нижнего регистра"...

Это также правдоподобно, но звучит как надуманное и менее ясное. Может ли кто-нибудь, у кого больше опыта в PBT, пролить свет?

EDIT: следуя источникам @Eric, я добрался до этот пост, и есть пример того, что я имею в виду (в разделе Применение категорий еще раз): для проверки метода times в (F#):

type Dollar(amount:int) =
member val Amount  = amount 
member this.Add add = 
    Dollar (amount + add)
member this.Times multiplier  = 
    Dollar (amount * multiplier)
static member Create amount  = 
    Dollar amount  

автор заканчивает тем, что пишет тест, который выглядит так:

let ``create then times should be same as times then create`` start multiplier = 
let d0 = Dollar.Create start
let d1 = d0.Times(multiplier)
let d2 = Dollar.Create (start * multiplier)      // This ones duplicates the code of Times!
d1 = d2

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


person Chirlo    schedule 19.06.2015    source источник
comment
@ Эрик, я добавил больше информации, основываясь на источниках презентации, которую вы упомянули.   -  person Chirlo    schedule 25.06.2015


Ответы (2)


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

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

  • size
  • ++
  • reverse
  • toUpperCase
  • contains

Например:

  • upcaseReverse(y) ++ upcaseReverse(x) == upcaseReverse(x ++ y)

Затем подумайте о том, что может сломаться, если реализация будет нарушена. Будет ли свойство недействительным, если:

  1. размер не сохранился?
  2. не все символы были в верхнем регистре?
  3. строка не была правильно перевернута?

1. на самом деле подразумевается 3. и я думаю, что указанное выше свойство будет нарушено для 3. Однако оно не будет нарушено для 2 (если бы, например, вообще не было верхнего регистра). Можем ли мы улучшить его? Что о:

  • upcaseReverse(y) ++ x.reverse.toUpper == upcaseReverse(x ++ y)

Я думаю, что это нормально, но не верьте мне и проведите тесты!

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

  1. составить другими методами
  2. посмотреть, есть ли равенства, которые, кажется, выполняются (такие вещи, как «обход туда и обратно», «идемпотентность» или «проверка модели» в презентации)
  3. проверьте, не сломается ли ваша собственность, если код неверен

Обратите внимание, что 1. и 2. реализованы библиотекой с именем QuickSpec, а 3. — "тестирование мутаций".

Приложение

О вашем редактировании: операция Times - это просто оболочка вокруг *, поэтому тестировать особо нечего. Однако в более сложном случае вы можете проверить, что операция:

  • имеет элемент unit
  • является ассоциативным
  • коммутативен
  • является распределительным с добавлением

Если какое-либо из этих свойств не работает, это будет большим сюрпризом. Если вы закодируете эти свойства как общие свойства для любого двоичного отношения T x T -> T, вы сможете очень легко повторно использовать их во всех контекстах (см. "законы" Scalaz Monoid).

Возвращаясь к вашему примеру upperCaseReverse, я бы написал 2 отдельных свойства:

 "upperCaseReverse must uppercase the string" >> forAll { s: String =>
    upperCaseReverse(s).forall(_.isUpper)
 }

 "upperCaseReverse reverses the string regardless of case" >> forAll { s: String =>
    upperCaseReverse(s).toLowerCase === s.reverse.toLowerCase
 }

Это не дублирует код и указывает на две разные вещи, которые могут сломаться, если ваш код неверен.

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

  • объединение тестируемой функции с другими функциями (.isUpper в первом свойстве)
  • сравнение тестируемой функции с более простой «моделью» вычислений («реверс независимо от регистра» во втором свойстве)
person Eric    schedule 19.06.2015
comment
Насколько я понимаю вашу точку зрения (и презентацию, на которую вы ссылаетесь), так это то, что PBT определяет свойства вокруг моего кода, например: он коммутирует или нет, он сочетается с другими методами... но эти свойства на самом деле не выражают то, что должен получить результат функции быть. Если я хочу иметь свойство, которое говорит, что upcaseReverse будет инвертировать и увеличивать любую строку, я не вижу другого способа проверить это, кроме как дублировать код. Предлагаемое вами свойство (upcaseReverse(y) ++ x.reverse.toUpper == upcaseReverse(x ++ y)) уже как-то дублирует код, просто вызывает методы в другом порядке. - person Chirlo; 25.06.2015
comment
Неееет, у меня было дополнительное редактирование, которое, видимо, потерялось. Я добавлю суть обратно. - person Eric; 26.06.2015
comment
Хорошо, я начинаю понимать вашу точку зрения. Я еще немного поработаю над портированием своих модульных тестов и вернусь к этому вопросу, спасибо! - person Chirlo; 26.06.2015

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

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

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

В других случаях наступает момент, когда вероятность поломки теста сводит на нет преимущество теста, обнаруживающего реальные проблемы. По этой причине, даже если это противоречит передовой практике в других отношениях (перечисление вещей, которые должны быть рассчитаны, а не написание СУХОГО кода), я стараюсь писать тестовый код, который в некотором роде проще, чем производственный код, поэтому он с меньшей вероятностью неудача.

Если я не могу найти способ написать код проще, чем тестовый код, который также удобен в сопровождении (читай: это мне тоже нравится), я переношу этот тест на более высокий уровень (например, модульный тест -> функциональный тест)

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

Для функционального тестирования часто бывает проще написать правило, которому должна удовлетворять функция, чем функцию, удовлетворяющую правилу. Мне это кажется очень похожим на проблему P vs NP. Где вы можете написать программу для ПРОВЕРКИ решения за линейное время, но все известные программы для ПОИСКА решения занимают гораздо больше времени. Это кажется прекрасным случаем для проверки свойств.

person janicedn    schedule 24.08.2020