Last active
September 16, 2020 14:07
-
-
Save davidallsopp/60d7474a1fe8dc9b1f2d to your computer and use it in GitHub Desktop.
Examples of writing mixed unit/property-based (ScalaTest with ScalaCheck) tests. Includes tables and generators as well as 'traditional' tests.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Examples of writing mixed unit/property-based (ScalaCheck) tests. | |
* | |
* Includes tables and generators as well as 'traditional' tests. | |
* | |
* @see http://www.scalatest.org/user_guide/selecting_a_style | |
* @see http://www.scalatest.org/user_guide/property_based_testing | |
*/ | |
import org.scalatest._ | |
import prop._ | |
import scala.collection.immutable._ // For BitSet | |
import java.awt.Color | |
/** | |
* This is the "scalatest" style of property-driven tests, which aims to be more consistent with the other ScalaTest | |
* styles. Note that the syntax has subtle differences from pure ScalaCheck, e.g. | |
* | |
* property("name") {forAll {}} rather than | |
* property("name") = forAll {} | |
*/ | |
class Example extends PropSpec with PropertyChecks with ShouldMatchers { | |
val examples = | |
Table( | |
"set", | |
BitSet.empty, | |
HashSet.empty[Int], | |
TreeSet.empty[Int]) | |
// table-driven | |
property("an empty Set should have size 0") { | |
//forAll(examples) { set => set.size should be(0) } | |
forAll(examples) { _.size should be(0) } | |
} | |
// table-driven, expecting exceptions | |
property("invoking head on an empty set should produce NoSuchElementException") { | |
forAll(examples) { set => | |
a[NoSuchElementException] should be thrownBy { set.head } | |
} | |
} | |
// table-driven, expecting exceptions, with an alternative syntax | |
property("again, invoking head on an empty set should produce NoSuchElementException") { | |
forAll(examples) { set => | |
evaluating { set.head } should produce[NoSuchElementException] | |
} | |
} | |
// A 2-column table | |
val colours = Table( | |
("Name", "Colour"), // First tuple is the title row | |
("r", Color.RED), | |
("g", Color.GREEN), | |
("b", Color.BLUE)) | |
property("colours") { | |
forAll(colours) { (name: String, colour: Color) => | |
colour.toString should include(s"$name=255") // e.g. java.awt.Color[r=0,g=255,b=0] | |
} | |
} | |
// A bit more concise with a case statement (don't need explicit types) | |
property("colours2") { | |
forAll(colours) { case (name, colour) => colour.toString should include(s"$name=255") } // e.g. java.awt.Color[r=0,g=255,b=0] | |
} | |
// generator-driven, but no surrounding property() | |
// This will run, but doesn't show up in the results, which is confusing - avoid! | |
forAll { (n: Int) => | |
whenever(n > 1) { n / 2 should be > 0 } | |
} | |
// whenever() is recommended even if generator only creates valid values - see the docs: | |
/* "Note that even if you are use generators that don't produce the invalid values, you still need the whenever clause. | |
* The reason is that once a property fails, ScalaCheck will try and shrink the values to the smallest values that | |
* still cause the property to fail. During this shrinking process ScalaCheck may pass invalid values. The whenever | |
* clause is still needed to guard against those values. (The whenever clause also clarifies to readers of the code | |
* exactly what the property is in a succinct way, without requiring that they find and understand the generator | |
* definitions.)" */ | |
property("make mine a half") { | |
forAll { (n: Int) => | |
whenever(n > 1) { n / 2 should be > 0 } | |
} | |
} | |
// Free-form test, no scalacheck magic at all, using matcher | |
property("matcher only") { | |
1 shouldEqual 1 | |
// See options for equality matching: | |
// http://www.scalatest.org/user_guide/using_matchers#checkingEqualityWithMatchers | |
} | |
// Free-form test, no scalacheck magic at all, using assertion | |
property("assert something") { | |
assert(Set.empty.size === 0) | |
} | |
// Do NOT do this - it has no effect (will always pass). Gives a compiler warning if you have the appropriate settings. | |
property("boolean only - broken!") { | |
1 == 0 | |
} | |
// Explicitly defined generators: | |
import org.scalacheck.Gen | |
val die = Gen.choose(1, 6) | |
property("outputPrecision") { | |
forAll(die)(_.toString.length == 1) | |
} | |
property("sum") { | |
forAll(die, die) { case (a, b) => a + b >= 2 && a + b <= 12 } | |
} | |
// case class trick - generate random instances if there are implicit generators | |
// for the constructor parameters: | |
case class Person(name: String, age: Int) | |
val people = Gen.resultOf(Person) // generator of people | |
// though ages may be negative, so need a whenever filter or | |
// similar. | |
def doSomething(p: Person) = true // always OK, just an example | |
property("all the people") { | |
forAll(people) { p => whenever(p.age >= 0) { doSomething(p) } } | |
} | |
// Can use any function in resultOf: | |
val another = Gen.resultOf((x: Int) => x + 1) | |
} | |
/** | |
* This is what ScalaTest calls the ScalaCheck style of property-driven test. | |
* See http://www.scalatest.org/user_guide/writing_scalacheck_style_properties#selectingAStyle | |
* | |
*/ | |
class Example2 extends PropSpec with Matchers with Checkers { | |
// No "forAll" needed here, though other examples use it | |
check((a: List[Int], b: List[Int]) => a.size + b.size == (a ::: b).size) | |
import org.scalacheck.Prop._ // needed for ==> etc | |
check { (n: Int) => n > 1 ==> n / 2 > 0 } | |
// Using ==> rather than "whenever" which is used in the ScalaTest style | |
} | |
// Funsuite example from http://stackoverflow.com/questions/21413782/sbt-doesnt-recognize-scalatest-table-driven-property-checks-as-tests | |
// (by Bill Venners himself) | |
class MySpec extends FunSuite with PropertyChecks { | |
test("give the test a name here") { | |
forAll { (x: Int, y: Int) => assert(true) } | |
} | |
} | |
/** | |
* flatspec. | |
* Names are mangled because duplicate test names give an error! | |
* and some of the names are nonsense because it's the code I'm trying out | |
*/ | |
class SetSpec extends FlatSpec with Matchers with PropertyChecks { | |
//behavior of "An empty Set" | |
"2) An empty Set" should "have size 0" in { | |
assert(Set.empty.size === 0) | |
} | |
it should "also have size 0" in { | |
assert(Set.empty.size === 0) | |
} | |
val examples = | |
Table( | |
"set", | |
BitSet.empty, | |
HashSet.empty[Int], | |
TreeSet.empty[Int]) | |
// table-driven | |
it should "again have size 0" in { | |
forAll(examples) { set => set.size should be(0) } | |
} | |
// generator-driven | |
"Number" should "divide by half" in { | |
forAll { (n: Int) => | |
whenever(n > 1) { n / 2 should be > 0 } | |
} | |
} | |
} |
@davidallsopp How to limit the number of properties generated? For instance, if I want to stop the test after having generated 10 values.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for posting this, I was having a hard time to check a few properties in FlatSpec. Only found out about PropertySpecs by reading this gist.