Раздел 21 · Haskell

Тестирование: hspec, QuickCheck и golden-тесты

middle-senior~35 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Тестирование: hspec, QuickCheck и golden-тесты

Генератор собран и оформлен пакетом. Теперь докажем, что он работает, и поймаем регрессии заранее. Три инструмента и три разных вопроса к коду. hspec и table-driven отвечают “на этих конкретных входах результат такой”. QuickCheck отвечает “вот свойство, которое должно держаться на любом входе”. Golden-тесты отвечают “вывод не изменился незаметно”. Вместе они закрывают чистое ядро генератора почти целиком, не трогая файловую систему.

Идея

Чистое ядро, которое мы выделили в уроке 28, окупается именно здесь. parse, convert, escape, process это чистые функции, а чистую функцию проверять одно удовольствие: дал вход, сравнил выход, никаких моков, никакой подготовки окружения. Это прямое продолжение раздела про тестирование, только теперь на Haskell, где чистота это не дисциплина, а свойство типа.

Три подхода дополняют друг друга:

  • Table-driven на hspec. Таблица пар вход-выход. Точно, читаемо, легко добавить случай. Ловит конкретные баги и служит документацией поведения.
  • Property-based на QuickCheck. Формулируешь свойство, библиотека сама генерирует сотни случайных входов и ищет контрпример. Ловит то, о чём ты не подумал.
  • Golden-тесты. Эталонный вывод лежит файлом на диске; тест сравнивает с ним. Ловит незаметные изменения вывода: рефакторинг, который случайно поменял один тег.

hspec: каркас и table-driven

hspec описывает тесты деревом describe/it. Точка входа собирает все спеки:

module Main (main) where

import Test.Hspec (hspec)
import qualified MarkupSpec
import qualified ConvertSpec
import qualified HtmlSpec
import qualified GoldenSpec

main :: IO ()
main = hspec $ do
  MarkupSpec.spec
  ConvertSpec.spec
  HtmlSpec.spec
  GoldenSpec.spec

Теперь самое полезное для парсера: таблица случаев. Вместо десяти почти одинаковых it заводим тип строки таблицы и прогоняем все одним mapM_.

module MarkupSpec (spec) where

import Test.Hspec
import Test.QuickCheck

import Press.Markup (Document, Structure (..), parse)

data Case = Case
  { caseName :: String
  , caseInput :: String
  , caseExpected :: Document
  }

cases :: [Case]
cases =
  [ Case "empty input is an empty document" "" []
  , Case "a single paragraph" "hello world" [Paragraph "hello world"]
  , Case "lines of one paragraph are joined" "hello\nworld" [Paragraph "hello world"]
  , Case "a blank line closes a paragraph" "one\n\ntwo" [Paragraph "one", Paragraph "two"]
  , Case "a heading" "* Title" [Heading 1 "Title"]
  , Case "adjacent dash lines collapse into one list"
      "- a\n- b\n- c" [UnorderedList ["a", "b", "c"]]
  , Case "a code block keeps its lines verbatim"
      "> line one\n> line two" [CodeBlock ["line one", "line two"]]
  , Case "a paragraph then a list"
      "intro\n\n- a\n- b" [Paragraph "intro", UnorderedList ["a", "b"]]
  ]

И прогон таблицы:

spec :: Spec
spec = describe "Press.Markup.parse" $ do
  describe "table-driven cases" $
    mapM_
      ( \(Case name input expected) ->
          it name $ parse input `shouldBe` expected
      )
      cases

Добавить случай теперь это одна строка в cases, а не новый it с копипастой. Таблица заодно читается как спецификация формата: вот что разметка обещает.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.