ScalaCheck
Property-Based testing
∀
Qui suis-je ?
- Benoît Lemoine
- Développeur FullStack (Scala / TypeScript) chez Captain Dash
- @benoit_lemoine
C'est quoi un test ?
- une procédure...
- ...automatisé...
- .. permettant de vérifier le bon fonctionnement d'un programme
Testing shows the presence, not the absence of bugs
Dijkstra.
Test unitaire
Tests basés sur l'exemple
"add should sum the numbers" in {
add(1, 2) shouldBe 3
add(123, 0) shouldBe 123
}
Implémentation respectant le test
def add(x:Int, y:Int):Int = (x, y) match {
case (1, 2) => 3
case (123, 0) => 123
case (_, _) => x
}
ScalaCheck
- Librairie Scala pour faire du test de propriété
- Utilisable seule
- Intégrée dans ScalaTest et Specs
Test de propriété
import org.scalacheck.Properties
import org.scalacheck.Prop.forAll
object AddSpecification extends Properties("add") {
property("0 should be right neutral") = forAll { (x: Int) =>
add(x, 0) == x
}
}
[info] + add.0 should be right neutral: OK, passed 100 tests.
Ce qu'il se passe en vrai
import org.scalacheck.Properties
import org.scalacheck.Prop.forAll
import org.scalacheck.Prop.collect
object AddSpecification extends Properties("add") {
property("0 should be right neutral") = forAll { (x: Int) =>
collect(x) {
add(x, 0) == x
}
}
}
Ce qu'il se passe en vrai
[info] + add.0 should be right neutral: OK, passed 100 tests.
[info] > Collected test data:
[info] 12% 2147483647
[info] 12% -2147483648
[info] 10% 0
[info] 9% 1
[info] 6% -1
[info] 1% -1691614918
[info] 1% 670895397
...
[info] 1% 1334365593
[info] 1% 1694241903
[info] 1% 425822646
[info] 1% -97876626
[info] 1% -625637529
En cas d'erreur
object AddSpecification extends Properties("add") {
property("0 should be left neutral") = forAll { (x: Int) =>
add(0, x) == x
}
}
[info] ! add.0 should be left neutral: Falsified after 0 passed tests.
[info] > ARG_0: 1
[info] > ARG_0_ORIGINAL: 256088374
Exemple Shrink
def pad(str:String, n:Int):String = if(str.length < n) {
str.padTo(n, '0')
} else {
str
}
Exemple Shrink
property("padded str length should be 10") =forAll {(str: String) =>
collect(str) {
pad(str, 10).length == 10
}
}
[info] ! add.padded str length should be 10: Falsified after 17 passed tests.
[info] > ARG_0: "蒨噯穉洽ᄼ嫕ᛊ˕皑"
[info] > ARG_0_ORIGINAL: "蒨ꎪ뱏㒫ⵏ豙噯穉洽ᄼ嫕ᛊ˕皑"
[info] > Collected test data:
[info] 28%
[info] 6% 纞⥯
[info] 6% 蒨噯穉洽ᄼ嫕ᛊ˕皑
...
[info] 6% 횏㞤㡛
[info] 6% שּ繏Гဘ욖䇨ࠢ⇱
[info] 6% 䲫褐ተ鳟
[info] 6% 㪢癫
[info] 6% 獒꽺啼᯽鲞浡硝⤜̇
[info] 6% 洭ॣࡨ盽㶴
À quoi ça sert ?
- Tests de propriété algébrique
- Associativité
myFn(x, myFn(y, z)) == myFn(myFn(x,y), z)
- Commutativité
myFn(x, y) == myFn(y, x)
- Element neutre
myFn(x, e) == x
- Element absorbant
myFn(x, e) == e
- etc.
Exemple
List
est un Functor
property("identity") = forAll {(list: List[Long]) =>
list.map(identity) == list
}
property("composite") = forAll {(list: List[Long],
fn1:Long => String,
fn2:String => Int) =>
list.map(fn1).map(fn2) == list.map(fn1 andThen fn2)
}
Exemple
de la vraie vie
deserialize(serialize(myObject)) == myObject
findById(save(entity)) == entity
compress(input).size <= input.size
Gen
val genInt:Gen[Int] = Gen.choose(10, 100)
forAll(genInt) { (x:Int) =>
x > 9 && x < 101
}
Créer un Gen
Gen.numStr
, Gen.alphaStr
Gen.choose(0, 1000)
Gen.oneOf("A", "E", "I", "O", "U")
Gen.option(myOtherGen)
Gen.listOf(myOtherGen)
Gen
val evenGen:Gen[Int] = Gen.choose(0, 1000).suchThat( _ % 2 == 0)
val anotherEvenGen:Gen[Int] = Gen.choose(0, 500).map(_ + _)
Gen est monadique
case class User(id:Int, name:String)
val genUser:Gen[User] = for {
id <- Gen.choose(0, Int.MaxValue)
name <- Gen.alphaStr.suchThat(_.length > 0)
} yield User(id, name)
forAll(genUser) { (user:User) =>
user.name.length > 0
}
Les arbitrary
val genUser:Gen[User] = for {
id <- Gen.choose(0, Int.MaxValue)
name <- Gen.alphaStr.suchThat(_.length > 0)
} yield User(id, name)
implicit val arbUser:Arbitrary[User] = Arbitrary(genUser)
forAll { (user:User) =>
user.name.length > 0
}
Exemple : java.time.LocalDate
val genLocalDate:Gen[LocalDate] = for {
year <- Gen.choose(1980, 2040)
month <- Gen.choose(1, 12)
day <- Gen.choose(1, Month.of(month).length(Year.isLeap(year)))
} yield LocalDate.of(year, month, day)
Flemme et Gen de case class
https://github.com/alexarchambault/scalacheck-shapeless
case class User(id:Int, firstName:String, lastName:String) {
def name = firstName + " " + lastName
}
import org.scalacheck.Shapeless._
forAll((user:User) => {
user.name.contains(user.firstName)
})
Flemme et Gen d'ADT
https://github.com/alexarchambault/scalacheck-shapeless
sealed trait Planetoid
case object Mercury extends Planetoid
case object Venus extends Planetoid
case class JovianMoon(name:String) extends Planetoid
import org.scalacheck.Shapeless._
forAll((planetoid:Planetoid) => {
planetoid.toString().length > 0
})
Attention !
Ne pas recoder la fonction dans le test
case class User(id:Int, firstName:String, lastName:String) {
def name = firstName + " " + lastName
}
forAll((user:User) => {
user.name == firstName + " " + lastName
})
Exemples du vrai monde
property("serialize and deserialize should be inverse") =
forAll { (user: User) =>
Json.toJson(user).asOpt[User] == Some(user)
}
property("what's saved in database can be retrieved") =
forAll { (user:User) =>
User.save(user).map(_.id).flatMap(User.findById) == Some(user)
}
Exemples du vrai monde
property("identity don't modify the functor") =
forAll { (response:HttpReponse[String]) =>
response.map(identity) == response
}
property("functor is associative") =
forAll { (response:HttpReponse[String],
f1: String => Int,
f2:Int => Long) =>
response.map(f1).map(f2) == response.map(f1 andThen f2)
}
Avec scalaz
implicit def respEqual[A]: Equal[HttpResponse[A]] = Equal.equalA
implicit val respFunctor = new Functor[HttpResponse] {
def map[A, B](fa: HttpResponse[A])(f: A => B): HttpResponse[B] =
fa.map(f)
}
functor.laws[HttpResponse].check
Dans les autres langages
- Quickcheck (Haskell)
- SwiftCheck (Swift)
- JsVerify (JavaScript)
- etc.
Bonus : Vérification de propriété par les types
-- Idris
total plusZeroLeftNeutral : (right : Nat) -> 0 + right = right
plusZeroLeftNeutral right = Refl
total plusZeroRightNeutral : (left : Nat) -> left + 0 = left
plusZeroRightNeutral Z = Refl
plusZeroRightNeutral (S n) =
let inductiveHypothesis = plusZeroRightNeutral n in
rewrite inductiveHypothesis in Refl