Статьи

Котлинтест и имущественное тестирование

Я был очень рад видеть, что Kotlintest , превосходный порт в Kotlin , поддерживает тестирование на основе свойств.

Я познакомился с тестированием на основе свойств благодаря великолепной книге «Функциональное программирование в Scala» .

Идея тестирования на основе свойств проста — поведение программы описывается как свойство, а среда тестирования генерирует случайные данные для проверки свойства. Это лучше всего проиллюстрировать на примере с использованием превосходной библиотеки scalacheck :

1
2
3
4
5
6
7
8
import org.scalacheck.Prop.forAll
import org.scalacheck.Properties
 
object ListSpecification extends Properties("List") {
  property("reversing a list twice should return the list") = forAll { (a: List[Int]) =>
    a.reverse.reverse == a
  }
}

scalacheck сгенерирует случайный список (целого числа) различных размеров и подтвердит, что это свойство относится к спискам. Подобная спецификация, выраженная через Kotlintest, выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
import io.kotlintest.properties.forAll
import io.kotlintest.specs.StringSpec
 
 
class ListSpecification : StringSpec({
    "reversing a list twice should return the list" {
        forAll{ list: List<Int> ->
            list.reversed().reversed().toList() == list
        }
    }
})

Если генераторы должны быть немного более ограничены, скажем, если мы хотим проверить это поведение в списках целых чисел в диапазоне от 1 до 1000, тогда явный генератор можно передать следующим образом, снова начиная с scalacheck:

1
2
3
4
5
6
7
8
9
import org.scalacheck.Prop.forAll
import org.scalacheck.{Gen, Properties}
 
object ListSpecification extends Properties("List") {
  val intList = Gen.listOf(Gen.choose(1, 1000))
  property("reversing a list twice should return the list") = forAll(intList) { (a: List[Int]) =>
    a.reverse.reverse == a
  }
}

и эквивалентный код котлинтеста:

01
02
03
04
05
06
07
08
09
10
11
12
13
import io.kotlintest.properties.Gen
import io.kotlintest.properties.forAll
import io.kotlintest.specs.StringSpec
 
class BehaviorOfListSpecs : StringSpec({
    "reversing a list twice should return the list" {
        val intList = Gen.list(Gen.choose(1, 1000))
 
        forAll(intList) { list ->
            list.reversed().reversed().toList() == list
        }
    }
})

Учитывая это, позвольте мне теперь перейти к другому примеру с сайта scalacheck , на этот раз, чтобы проиллюстрировать ошибку:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import org.scalacheck.Prop.forAll
import org.scalacheck.Properties
 
object StringSpecification extends Properties("String") {
 
  property("startsWith") = forAll { (a: String, b: String) =>
    (a + b).startsWith(a)
  }
 
  property("concatenate") = forAll { (a: String, b: String) =>
    (a + b).length > a.length && (a + b).length > b.length
  }
 
  property("substring") = forAll { (a: String, b: String, c: String) =>
    (a + b + c).substring(a.length, a.length + b.length) == b
  }
}

второе свойство, описанное выше, неверно — если две строки соединены вместе, они ВСЕГДА больше, чем каждая из частей, это неверно, если одна из строк пуста. Если бы я должен был запустить этот тест с использованием scalacheck, он правильно улавливает это неправильно указанное поведение:

1
2
3
4
5
6
+ String.startsWith: OK, passed 100 tests.
! String.concatenate: Falsified after 0 passed tests.
> ARG_0: ""
> ARG_1: ""
+ String.substring: OK, passed 100 tests.
Found 1 failing properties.

Эквивалент котлинтеста следующий:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
import io.kotlintest.properties.forAll
import io.kotlintest.specs.StringSpec
 
class StringSpecification : StringSpec({
    "startsWith" {
        forAll { a: String, b: String ->
            (a + b).startsWith(a)
        }
    }
 
    "concatenate" {
        forAll { a: String, b: String ->
            (a + b).length > a.length && (a + b).length > b.length
        }
    }
 
    "substring" {
        forAll { a: String, b: String, c: String ->
            (a + b + c).substring(a.length, a.length + b.length) == b
        }
    }
})

при запуске корректно обнаруживает проблему с помощью concatenate и выдает следующий результат:

1
2
3
4
5
java.lang.AssertionError: Property failed for
 
Y{_DZ<vGnzLQHf9|3$i|UE,;!%8^SRF;JX%EH+<5d:p`Y7dxAd;I+J5LB/:O)
 
 at io.kotlintest.properties.PropertyTestingKt.forAll(PropertyTesting.kt:27)

Однако здесь есть проблема: scalacheck обнаружил более простой случай сбоя, он делает это с помощью процесса, называемого «Минимизация тестового набора», где в случае сбоя он пытается найти наименьший тестовый случай, который может дать сбой, то, что может узнать Kotlintest. от.

Существуют и другие особенности, в которых Kotlintest отстает по отношению к scalacheck, большая из которых может комбинировать генераторы:

1
2
3
4
5
6
7
8
case class Person(name: String, age: Int)
 
val genPerson = for {
  name <- Gen.alphaStr
  age <- Gen.choose(1, 50)
} yield Person(name, age)
 
genPerson.sample

Однако в целом я считаю, что DSL Kotlintest и его поддержка тестирования, основанного на свойствах, являются хорошим началом и с нетерпением ожидаем развития этой библиотеки с течением времени.

Если вы хотите немного поиграть с этими сэмплами, они доступны в моем репозитории github здесь — https://github.com/bijukunjummen/kotlintest-scalacheck-sample

Ссылка: Kotlintest и имущественное тестирование от нашего партнера JCG Biju Kunjummen в блоге all and sundry.