Reputation: 112
I'm using ScalaCheck to do some property-based tests in ScalaTest. Say I want to test a function, f(x: Double): Double
that is only defined for x >= 0.0
, and which returns NaN
for arguments outside of that domain. Ideally, I'd like to do something like this:
import org.scalatest.FunSpec
import org.scalatest.prop.GeneratorDrivenPropertyChecks
def f(x: Double) = Math.sqrt(x) // The actual function isn't important.
class FTest
extends FunSpec
with GeneratorDrivenPropertyChecks {
describe("f(x)") {
it("must accept every argument value and handle it correctly") {
forAll { x: Double =>
val r = f(x)
if(x >= 0.0) assert(!r.isNaN && r === Math.sqrt(x)) // Too simplistic, I know. ;-)
else assert(r.isNaN)
}
}
}
}
Now, this is fairly elegant and works, but I'm concerned about boundary checking, because I doubt that - in the general case - ScalaCheck is going to be able to find the boundary and test that the function responds correctly with the values either side of that boundary (>= 0.0 in this case). Of course, I can separate the two conditions using whenever
(ScalaTest's replacement for ScalaCheck's ==>
operator), but that's more effort and a lot of generated values are wasted:
class FTest2
extends FunSpec
with GeneratorDrivenPropertyChecks {
describe("f(x)") {
it("must accept every valid argument value and handle it correctly") {
forAll { x: Double =>
whenever(x >= 0.0) {
val r = f(x)
assert(!r.isNaN && r === Math.sqrt(x))
}
}
}
it("must report the correct error value for invalid argument values") {
forAll { x: Double =>
whenever(x < 0.0) assert(f(x).isNaN)
}
}
}
}
(I know that I can also use a customer generator to limit the range so that whenever
is not required, but I think that's beside the point. Feel free to correct me if I'm wrong about that.)
So, what I'm curious about is:
Thanks for your assistance~
Upvotes: 4
Views: 317
Reputation: 66244
ScalaCheck cannot automatically figure out which values your function treats as valid; you need to either encode this information in your properties (using something like whenever
) or in your generators. Which approach to pick is context-specific.
Keeping properties "small" is preferable: focused, orthogonal properties are easier to read/write/maintain, and you can always compose them later to build more comprehensive properties. Therefore, I would keep the two properties (happy and unhappy cases) separate.
To avoid "wasting" generated values, I would use two separate generators (one for non-negative doubles and another for negative doubles); no need for whenever
with that approach.
val genNonnegativeDouble: Gen[Double] = Gen.choose(0, Double.MaxValue)
val genNegativeDouble: Gen[Double] = Gen.negNum[Double]
Your properties would then look as follows:
final class FTest2
extends FunSpec
with GeneratorDrivenPropertyChecks {
describe("f") {
it("must accept every valid argument value and handle it correctly") {
forAll(genNonnegativeDouble) { x =>
val r = f(x)
assert(!r.isNaN && r === Math.sqrt(x))
}
}
it("must report the correct error value for invalid argument values") {
forAll(negativeDouble) { x =>
assert(f(x).isNaN)
}
}
}
}
Incidentally,
Double.NaN
to indicate failure; an Option[Double]
is a good candidate, here, as there is only one reason for failure.Upvotes: 3