munit-scalacheck — это библиотека интеграции, которая объединяет два мощных инструмента тестирования в экосистеме Scala:

  1. MUnit — современный, сверхбыстрый и легковесный фреймворк для юнит-тестирования, созданный специально под Scala 3 (с нативной поддержкой как JVM, так и Scala.js).
  2. ScalaCheck — золотой стандарт для Property-Based Testing (тестирования на основе свойств) в Scala (вдохновленный библиотекой QuickCheck из Haskell).

Библиотека munit-scalacheck позволяет писать тесты ScalaCheck прямо внутри привычных тест-классов MUnit, используя удобное ключевое слово property(...) вместо стандартного test(...).


Что такое Property-Based Testing (Тестирование на основе свойств)?

В обычном юнит-тестировании мы используем тестирование на примерах (Example-Based Testing):

  • Мы вручную пишем конкретные входные данные: «Дана начальная позиция и кубики [1, 2].»
  • Мы вручную пишем конкретный ожидаемый результат: «Должно быть ровно 20 легальных ходов.»

В Property-Based Testing мы не придумываем примеры сами. Вместо этого мы описываем инварианты (свойства) — правила, которые должны оставаться истинными для любых входных данных.

Пример свойства для Dice Chess:

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

ScalaCheck берет это свойство и автоматически генерирует 100 (или 1000) абсолютно случайных комбинаций позиций и кубиков, проверяя, не сломается ли наш код на каком-нибудь безумном edge-case.


Для чего munit-scalacheck нужен нашему шахматному движку?

Для движка Dice Chess такое тестирование — это ультимативное оружие против багов. И вот почему:

1. Борьба с комбинаторным взрывом 💥

Шахматы имеют колоссальное пространство состояний ( позиций). Добавление случайных кубиков и микроходов увеличивает число возможных сценариев в разы. Написать ручные тесты на все случаи блокировок, шахов, рокировок и сложных превращений пешек физически невозможно. ScalaCheck находит сценарии, о которых человек-разработчик даже не задумается (например: «Черный слон заблокирован, белый король под шахом на краю доски, а на кубиках выпали рокировка и пешка»).

2. Автоматический поиск контрпримеров (Fuzzing) 🔍

Если алгоритм фильтрации ходов содержит логическую ошибку, ScalaCheck быстро сгенерирует случайную позицию, на которой свойство не выполнится, и выдаст её нам.

3. Умное сжатие ошибок (Shrinking) 📉

Это одна из самых крутых фич ScalaCheck. Если фреймворк нашел баг на сложнейшей позиции с 32 фигурами и кубиками [1, 4, 6], он не просто выплюнет эту позицию. Он начнет автоматически её упрощать: убирать лишние фигуры с доски, менять кубики на более простые, пока не найдет минимально возможную позицию, на которой баг всё еще воспроизводится. Это делает отладку невероятно простой!


Как это выглядит в коде?

Вот простой математический пример:

import munit.ScalaCheckSuite
import org.scalacheck.Prop._
 
class MathSpec extends ScalaCheckSuite {
 
  // Вместо test() пишем property()
  property("сложение коммутативно (a + b == b + a)") {
    // forAll автоматически сгенерирует сотни случайных чисел a и b
    forAll { (a: Int, b: Int) =>
      a + b == b + a
    }
  }
}

А вот пример из нашего созданного файла MutableLegalMovesFilterSpec.scala (Area D):

  // Мы создали генератор случайных бросков (3 числа от 1 до 6)
  val diceGen: Gen[List[Int]] =
    Gen.listOfN(3, Gen.choose(1, 6))
 
  property("D3: Все отфильтрованные ходы являются подмножеством псевдолегальных") {
    // forAll берет случайную позицию из базы и случайный бросок
    forAll(gameStateGen, diceGen) { (state, dice) =>
      val legal     = filterMoves(state, dice)
      val allPseudo = dice.distinct.flatMap(d => MoveGenerator.generateMoves(state, d))
 
      // Свойство: отфильтрованный список ходов не может содержать
      // ход, который вообще не генерировался кубиками!
      legal.forall(allPseudo.contains)
    }
  }

Резюме:

munit-scalacheck — это инструмент, который превращает скучное написание сотен однотипных юнит-тестов в проектирование строгих математических свойств. Он гарантирует высочайшую надежность и стабильность движка Dice Chess при любых случайных игровых ситуациях!