Мы ищем жизнеспособный шаблон проектирования для создания Scalacheck Gen
(генераторов), который может создавать как положительные, так и отрицательные тестовые сценарии. Это позволит нам запустить forAll
тестов для проверки функциональности (положительные случаи), а также убедиться, что наша проверка класса случаев работает правильно, отказавшись от всех недопустимых комбинаций данных.
Сделать простой параметризованный Gen
, который делает это на разовой основе, довольно легко. Например:
def idGen(valid: Boolean = true): Gen[String] = Gen.oneOf(ID.values.toList).map(s => if (valid) s else Gen.oneOf(simpleRandomCode(4), "").sample.get)
С помощью вышеизложенного я могу получить действительный или недействительный идентификатор для целей тестирования. Действительный, я использую, чтобы убедиться, что бизнес-логика преуспевает. Недействительный я использую, чтобы убедиться, что наша логика проверки отклоняет класс case.
Итак, проблема в том, что в больших масштабах это становится очень громоздким. Скажем, у меня есть контейнер данных с сотней различных элементов. Создать «хорошее» легко. Но теперь я хочу сгенерировать «плохой», и, кроме того:
Я хочу сгенерировать плохой для каждого элемента данных, где один элемент данных плохой (так что, как минимум, не менее 100 плохих экземпляров, проверяя, что каждый недопустимый параметр улавливается логикой проверки).
Я хочу иметь возможность переопределять конкретные элементы, например, вводить неверный идентификатор или неверный "foobar". Что бы это ни было.
Один шаблон, который мы можем использовать для вдохновения, — это apply
и copy
, которые позволяют нам легко создавать новые объекты, указывая переопределенные значения. Например:
val f = Foo("a", "b") // f: Foo = Foo(a,b)
val t = Foo.unapply(f) // t: Option[(String, String)] = Some((a,b))
Foo(t.get._1, "c") // res0: Foo = Foo(a,c)
Выше мы видим основную идею создания мутирующего объекта из шаблона другого объекта. На Scala это проще выразить так:
val f = someFoo copy(b = "c")
Используя это как вдохновение, мы можем думать о наших целях. Несколько вещей, о которых стоит подумать:
Во-первых, мы могли бы определить карту или контейнер ключей/значений для элемента данных и сгенерированного значения. Это можно использовать вместо кортежа для поддержки изменения именованного значения.
Учитывая контейнер пар ключ/значение, мы могли бы легко выбрать одну (или несколько) пар случайным образом и изменить значение. Это поддерживает цель создания набора данных, в котором одно значение изменено для создания ошибки.
Имея такой контейнер, мы можем легко создать новый объект из недопустимого набора значений (используя либо
apply()
, либо какой-либо другой метод).В качестве альтернативы, возможно, мы можем разработать шаблон, который использует кортеж, а затем просто
apply()
, что-то вроде методаcopy
, пока мы можем случайным образом изменить одно или несколько значений.
Вероятно, мы можем исследовать разработку многоразового шаблона, который делает что-то вроде этого:
def thingGen(invalidValueCount: Int): Gen[Thing] = ???
def someTest = forAll(thingGen) { v => invalidV = v.invalidate(1); validate(invalidV) must beFalse }
В приведенном выше коде у нас есть генератор thingGen
, который возвращает (действительно) Things
. Затем для всех возвращаемых экземпляров мы вызываем общий метод invalidate(count: Int)
, который случайным образом делает недействительными count
значений, возвращая недопустимый объект. Затем мы можем использовать это, чтобы убедиться, что наша логика проверки работает правильно.
Для этого потребуется определить функцию invalidate()
, которая, учитывая параметр (либо по имени, либо по положению), может затем заменить идентифицированный параметр значением, которое, как известно, является неверным. Это подразумевает наличие «анти-генератора» для определенных значений, например, если идентификатор должен состоять из 3 символов, тогда он знает, что нужно создать строку длиной не 3 символа.
Конечно, чтобы аннулировать известный единственный параметр (чтобы ввести неверные данные в тестовое условие), мы можем просто использовать метод копирования:
def thingGen(invalidValueCount: Int): Gen[Thing] = ???
def someTest = forAll(thingGen) { v => v2 = v copy(id = "xxx"); validate(v2) must beFalse }
Это сумма моих мыслей на сегодняшний день. Я лаю не на то дерево? Существуют ли хорошие шаблоны, которые справляются с таким тестированием? Любые комментарии или предложения о том, как лучше всего подойти к этой проблеме тестирования нашей логики проверки?