Property-based Testing

Как прекратить сочинять примеры для юнит-тестов и начать жить.

Property-based Testing

Как прекратить сочинять примеры для юнит-тестов и начать жить.

Александр Паньшин

«Простой» пример

Как это тестировать?

Типичный assertion


            expected == actual
        

Assertion в тестах ассистента


            WAT? == actual
        

"Свежая" мысль

А что, если проверками сравнивать не с примером... А просто проверять какие-нибудь свойства объекта?

Но для этого надо сначала определиться с требованиями.

Требования

Что насчет Arrange?

А теперь для каждого требования нам надо придумать тестовые примеры.

И реализовать все это!

Генерируй это!

Требования

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

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

Для любого пользователя, Ассистент должен содержать еще не выполненные задания.

Иногда падающие тесты

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

Запусти тесты сто раз!

Это и есть property-based testing

Property-based testing

Пример теста

property("Contains only elements for my children") =
    forAll { user: User =>
        val elements: Seq[Element] = Assistant.generate(user).elements
        elements forall { element =>
            user.children contains element.child
        }
    }

Еще пример теста

property("MUST NOT contain completed tasks") =
    forAll { user: User =>
        val elements: Seq[Element] = Assistant.generate(user).elements
        elements
            .find(element => element.task.isCompleted(user, element.child))
            .isEmpty
    }

workshop

Задача

Допустим, у нас есть алгоритм реализации кучи. Этот алгоритм поддерживает простые операции с кучей: insert, isEmpty, meld, findMin и deleteMin.

trait Heap {
  type H // type of a heap
  type A // type of an element
  def ord: Ordering[A] // ordering on elements
  def empty: H // creates empty heap
  def isEmpty(h: H): Boolean // whether the given heap h is empty
  def insert(x: A, h: H): H // the heap resulting from inserting x into h
  def meld(h1: H, h2: H): H // the heap resulting from merging h1 and h2
  def findMin(h: H): A // a minimum of the heap h
  def deleteMin(h: H): H // a heap resulting from deleting a minimum of h
}
trait IntHeap extends Heap {
  override type A: Int
  override def ord = scala.math.Ordering.Int
}

Некая реализация

В общем-то не важно, как это работает, важно, что оно определяет тип H:

object BinomialHeap extends Heap with IntHeap {
  type Rank = Int
  case class Node(x: A, r: Rank, c: List[Node])
  override type H = List[Node]
}

Свойства кучи

  property("empty") = forAll { (a: Int) =>
    val heap = insert(a, empty)
    findMin(heap) == a
  }
  property("isEmpty on empty returns true") = forAll { (a: Int) =>
    isEmpty(empty)
  }

Свойства кучи

  property("findMin") = forAll { (a: Int, b: Int) =>
    val heap = insert(a, insert(b, empty))
    (a min b) == findMin(heap)
  }
  property("deleteMin") = forAll { (a: Int) =>
    val heap = insert(a, empty)
    empty == deleteMin(heap)
  }

Свойства кучи

property("meld contains all items of both heaps") =
  forAll { (heap1: H, heap2: H) =>
    val elementsInBothHeaps = heapToList(heap1) ++ heapToList(heap2)
    elementsInBothHeaps.sorted == heapToList(meld(heap1, heap2))
  }

Упс!

[error] /.../test/scala/HeapSpecification.scala:46:
could not find implicit value for parameter a1:
org.scalacheck.Arbitrary[example.BinomialHeap.H]
[error]   property("meld contains all items of both heaps") =
            forAll { (heap1: H, heap2: H) =>

Нам нужен генератор!

Нам нужна случайная куча, но компилятор не знает, как ее создать или где взять.

Создадим генератор?

lazy val genHeap: Gen[H] = for {
    // get random value of type A
    randomValue <- arbitrary[A]
    // choose empty heap or random generated one
    randomHeap <- oneOf(const(empty), genHeap)
  } yield insert(randomValue, randomHeap)

implicit lazy val arbitraryHeap: Arbitrary[H] = Arbitrary(genHeap)

Сила — в генераторах!

И самая большая боль property-based testing тоже. Написать хороший генератор бывает очень сложно. Особенно для более «живых» примеров, чем небольшая структура.

Генератор пользователя для Ассистента

Это жесть же!

Генераторы нужно переиспользовать!

Зато такой генератор подойдет для тестирования почти любой части приложения.

Что скачать?

PHP

composer require steos/php-quickcheck dev-master
QuickCheck Primer for PHP Developers

Python

pip install hypothesis
Read the docs

%languageName%

Gist со ссылками на фреймворки на примерно десятке языков

Спасибо!