scalabling
scalabling

Reputation: 112

ScalaCheck valid/invalid test boundary

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:

  1. Is there a way to hint to ScalaCheck what the boundary value is, and ensure that it picks that value and the values either side of it?
  2. Is there any alternative to this that is equally as elegant, but which does a better job of finding the boundary automatically?

Thanks for your assistance~

Upvotes: 4

Views: 317

Answers (1)

jub0bs
jub0bs

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,

  • You should probably return something other than Double.NaN to indicate failure; an Option[Double] is a good candidate, here, as there is only one reason for failure.
  • You should only check doubles for approximate equality.

Upvotes: 3

Related Questions