I have functions A => Double
. I want to check whether two such functions give the same results (up to a tolerance, using the existing beCloseTo
matcher) for a given set of values.
I want to be able to write:
type TF = A => Double
(f: TF) must computeSameResultsAs(g: TF,tolerance: Double, tests: Set[A])
I want to build this matcher in a modular way, not simply writing a Matcher[TF]
from scratch.
It might be even nicer if I could write:
(f: TF) must computeSameResultsAs(g: TF)
.withTolerance(tolerance)
.onValues(tests: Set[A])
Also I want to get a reasonable description when the matcher fails.
After sleeping over it I came up with the following.
def computeSameResultsAs[A](ref: A => Double, tolerance: Double, args: Set[A]): Matcher[A => Double] =
args.map(beCloseOnArg(ref, tolerance, _)).reduce(_ and _)
def beCloseOnArg[A](ref: A => Double, tolerance: Double, arg: A): Matcher[A => Double] =
closeTo(ref(arg), tolerance) ^^ ((_: A => Double).apply(arg))
This is much shorter than Eric's solution but doesn't provide a good failure message. What I'd love to be able is rename the mapped value in the second method. Something like the following (which does not compile).
def beCloseOnArg[A](ref: A => Double, tolerance: Double, arg: A): Matcher[A => Double] =
closeTo(ref(arg), tolerance) ^^ ((_: A => Double).apply(arg) aka "result on argument " + arg)
If you want to write things with the second version you need to create a new Matcher
class encapsulating the functionality of the beCloseTo
matcher:
def computeSameResultsAs[A](g: A => Double,
tolerance: Double = 0.0,
values: Seq[A] = Seq()) = TFMatcher(g, tolerance, values)
case class TFMatcher[A](g: A => Double,
tolerance: Double = 0.0,
values: Seq[A] = Seq()) extends Matcher[A => Double] {
def apply[S <: A => Double](f: Expectable[S]) = {
// see definition below
}
def withTolerance(t: Double) = TFMatcher(g, t, values)
def onValues(tests: A*) = TFMatcher(g, tolerance, tests)
}
This class allows to use the syntax you're after:
val f = (i: Int) => i.toDouble
val g = (i: Int) => i.toDouble + 0.1
"f must be close to another similar function with a tolerance" in {
f must computeSameResultsAs[Int](g).withTolerance(0.5).onValues(1, 2, 3)
}
Now, let's see how to reuse the beCloseTo
matcher in the apply
method:
def apply[S <: A => Double](f: Expectable[S]) = {
val res = ((v: A) => beCloseTo(g(v) +/- tolerance).apply(theValue(f.value(v)))).forall(values)
val message = "f is "+(if (res.isSuccess) "" else "not ")+
"close to g with a tolerance of "+tolerance+" "+
"on values "+values.mkString(",")+": "+res.message
result(res.isSuccess, message, message, f)
}
In the code above, we apply a function returning a MatcherResult
to a sequence of values:
((v: A) => beCloseTo(g(v) +/- tolerance).apply(theValue(f.value(v)))).forall(values)
Note that:
f
is an Expectable[A => Double]
so we need to take its actual value
to be able to use it
similarly we can only apply an Expectable[T]
to a Matcher[T]
so we need to use the method theValue
to transform f.value(v)
to an Expectable[Double]
(from the MustExpectations
trait)
Finally, we when have the result of the forall
matching, we can customize the result messages by using:
the inherited result
method building a MatchResult
(what the apply
method of any Matcher
should return
passing it a boolean saying if the execution of beCloseTo
was successful: .isSuccess
passing it nicely formatted "ok" and "ko" messages, based on the input and on the result message of the beCloseTo
matching
passing it the Expectable
which was used to do the matching in the first place: f
, so that the final result has a type of MatchResult[A => Double]
I'm not sure how more modular we can get given your requirements. It looks to me that the best we can do here is to reuse beCloseTo
with forall
.
UPDATE
A shorter answer might be something like this:
val f = (i: Int) => i.toDouble
val g = (i: Int) => i.toDouble + 1.0
"f must be close to another similar function with a tolerance" in {
f must computeSameResultsAs[Int](g, tolerance = 0.5, values = Seq(1, 2, 3))
}
def computeSameResultsAs[A](ref: A => Double, tolerance: Double, values: Seq[A]): Matcher[A => Double] = (f: A => Double) => {
verifyFunction((a: A) => (beCloseTo(ref(a) +/- tolerance)).apply(theValue(f(a)))).forall(values)
}
The code above creates a failure message like:
In the sequence '1, 2, 3', the 1st element is failing: 1.0 is not close to 2.0 +/- 0.5
This should almost work out-of-the-box. The missing part is an implicit conversion from A => MatchResult[_]
to Matcher[A]
(which I'm going to add to the next version):
implicit def functionResultToMatcher[T](f: T => MatchResult[_]): Matcher[T] = (t: T) => {
val result = f(t)
(result.isSuccess, result.message)
}
You can use foreach
instead of forall
if you want to get all the failures:
1.0 is not close to 2.0 +/- 0.5; 2.0 is not close to 3.0 +/- 0.5; 3.0 is not close to 4.0 +/- 0.5
UPDATE 2
This gets better everyday. With the latest specs2 snapshot you can write:
def computeSameResultsAs[A](ref: A => Double, tolerance: Double, values: Seq[A]): Matcher[A => Double] = (f: A => Double) => {
((a: A) => beCloseTo(ref(a) +/- tolerance) ^^ f).forall(values)
}
UPDATE 3
And now with the latest specs2 snapshot you can write:
def computeSameResultsAs[A](ref: A => Double, tolerance: Double, values: Seq[A]): Matcher[A => Double] = (f: A => Double) => {
((a: A) => beCloseTo(ref(a) +/- tolerance) ^^ ((a1: A) => f(a) aka "the value")).forall(values)
}
The failure message will be:
In the sequence '1, 2, 3', the 1st element is failing: the value '1.0' is not close to 2.0 +/- 0.5