#haskell #quickcheck
#haskell #быстрая проверка
Вопрос:
В моем приложении есть следующие вещи:
newtype User = User Text
newtype Counts = Counts (Map User Int)
subjectUnderTest :: Counts -> Text
Примером правильного вывода может быть
> subjectUnderTest $ fromList [(User "foo", 4), (User "bar", 4), (User "qux", 2)]
"4: foo, barn2: qux"
Я хотел бы написать тесты на основе свойств, которые проверяют такие вещи, как «все пользователи представлены в выходных данных», «все счетчики представлены в выходных данных» и «все пользователи находятся в той же строке, что и их соответствующий счетчик». Общим для этих свойств является то, что их формулировка начинается с «все …»
Как мне написать свойство, которое проверяет, что что-то действительно для каждого элемента в Map
карте?
Ответ №1:
Я предполагаю, что этот вопрос является лишь упрощенным представлением чего-то более сложного, поэтому вот несколько стратегий, которые следует учитывать:
Разделите функциональность
Похоже subjectUnderTest
, это две несвязанные вещи:
- Он группирует значения на карте по значению, а не по ключу.
- Он форматирует или печатает перевернутую карту.
Если вы можете разделить функциональность на эти два шага, их легче тестировать изолированно.
Первый шаг вы можете сделать параметрически полиморфным. Вместо тестирования функции с типом Counts -> Text
, рассмотрите возможность тестирования функции с типом Eq b => Map a b -> [(b, [a])]
. Тестирование на основе свойств проще с параметрическим полиморфизмом, потому что вы получаете определенные свойства бесплатно. Например, вы можете быть уверены, что значения на выходе могут поступать только из входных данных, потому что нет способа вызвать a
и b
значения из воздуха.
Вам все равно придется писать тесты для свойств, о которых вы спрашиваете. Напишите функцию с типом like Eq b => Map a b -> Testable
. Если вы хотите проверить наличие всех значений, извлеките их из карты и составьте из них список. Отсортируйте список и nub
его. Теперь это [b]
значение. Это ваш ожидаемый результат.
Теперь вызовите свою функцию. Он возвращает что-то вроде [(b, [a])]
. Сопоставьте это с помощью fst
, отсортируйте и nub
это. Этот список должен быть равен вашему ожидаемому результату.
Следующий шаг (pretty-printing) см. В следующем разделе.
Обходы
Когда вы хотите использовать красивую печать на основе свойств, самый простой подход обычно заключается в том, чтобы стиснуть зубы и также написать синтаксический анализатор. Принтер и анализатор должны быть двойственными друг другу, поэтому, если у вас есть функция MyType -> String
, у вас должен быть анализатор с типом String -> Maybe MyType
.
Теперь вы можете написать общее свойство, например MyType -> Testable
. Он принимает в качестве входных данных значение MyType
(назовем его expected
). Теперь вы создаете значение (назовем его actual
) как actual = parse $ print expected
. Теперь вы можете это проверить Just expected === actual
.
Если важен конкретный String
формат, я бы привел несколько реальных примеров, используя старые добрые параметризованные тесты.
То, что вы проводите тестирование на основе свойств, не означает, что «обычный» модульный тест также не может быть полезным.
Пример
Вот простой пример того, что я имел в виду выше. Предположим, что
invertMap :: (Ord b, Eq b) => Map a b -> [(b, [a])]
вы можете определить одно из свойств как:
allValuesAreNowKeys :: (Show a, Ord a) => Map k a -> Property
allValuesAreNowKeys m =
let expected = nub $ sort $ Map.elems m
actual = invertMap m
in expected === nub (sort $ fmap fst actual)
Поскольку это свойство по-прежнему параметрически полиморфно, вам нужно будет добавить его в свой набор тестов с определенным типом, например:
tests = [
testGroup "Sorting Group 1" [
testProperty "all values are now keys" (allValuesAreNowKeys :: Map String Int -> Property)]]
Есть более красивые способы определения списков свойств; это просто шаблон, используемый шаблоном стека quickcheck-test-framework…
Комментарии:
1. В
containers
пакете есть модуль утилит list, состоящий в основном, если не полностью, из более эффективных вариантовnub
.2. Спасибо за отличный ответ! Вопрос на самом деле удивительно близок к проблеме, которую я на самом деле решаю — я изменил несколько имен, и входные строки, очевидно, составлены, но в остальном и тип, и формат вывода — это именно та проблема, над которой я работаю. Это меняет какую-либо часть вашего ответа? Мне удалось придумать какой-то хак, создающий a
[Expectation]
и использующийconjoin
его для создания aExpectation
(оказывается,Expectation
это моноид!), Который, кажется, проверяет то, что я хочу, но выдает ужасные сообщения об ошибках, когда тесты завершаются неудачно. Я проверю ваш подход!3. @TomasAschan FWIW Я добавил пример.