以模块化方式创建 Specs2 匹配器

发布于 2024-12-12 03:17:17 字数 1178 浏览 0 评论 0原文

我有函数 A =>;双倍。我想检查两个这样的函数对于给定的一组值是否给出相同的结果(在容差范围内,使用现有的 beCloseTo 匹配器)。

我希望能够写:

type TF = A => Double
(f: TF) must computeSameResultsAs(g: TF,tolerance: Double, tests: Set[A])

我想以模块化的方式构建这个匹配器,而不是简单地从头开始编写 Matcher[TF]

如果我可以写的话可能会更好:

(f: TF) must computeSameResultsAs(g: TF)
               .withTolerance(tolerance)
               .onValues(tests: Set[A])

另外,我想在匹配器失败时得到合理的描述。

编辑

睡了一觉后,我想出了以下内容。

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))

这比 Eric 的解决方案短得多,但没有提供良好的失败消息。我希望能够在第二种方法中重命名映射值。类似于以下内容(无法编译)。

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)

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.

Edit

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)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

小鸟爱天空丶 2024-12-19 03:17:17

如果您想使用第二个版本编写内容,则需要创建一个新的 Matcher 类来封装 beCloseTo 匹配器的功能:

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)
}

此类允许使用您正在使用的语法之后:

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)          
}

现在,让我们看看如何在 apply 方法中重用 beCloseTo 匹配器:

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)
 }

在上面的代码中,我们应用了一个返回 MatcherResult 的函数<一href="http://etorreborre.github.com/specs2/guide/org.specs2.guide.Matchers.html#Matching+with+a+sequence+of+values" rel="noreferrer">到一系列值:

((v: A) => beCloseTo(g(v) +/- tolerance).apply(theValue(f.value(v)))).forall(values)

请注意:

  1. f 是一个 Expectable[A =>; Double] 因此我们需要获取其实际的才能使用它

  2. 同样,我们只能将Expectable[T]应用于< code>Matcher[T] 因此我们需要使用 theValue 方法将 f.value(v) 转换为 Expectable[Double]< /代码>(来自MustExpectations 特征)

最后,我们得到的结果是对于所有匹配,我们可以使用以下方法自定义结果消息:

  1. 继承的result方法构建MatchResult应用的内容任何 Matcher 方法都应该返回

  2. 向其传递一个布尔值,表示 beCloseTo 的执行是否成功: .isSuccess

  3. 根据输入和 beCloseTo 的输入和结果消息,传递格式良好的“ok”和“ko”消息>匹配

  4. 向其传递Expectable code> 最初用于进行匹配:f,因此最终结果的类型为 MatchResult[A =>; Double]

我不确定根据您的要求我们可以得到多少模块化。在我看来,我们在这里能做的最好的事情就是重用 beCloseToforall

更新

一个较短的答案可能是这样的:

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)
}

上面的代码创建了一条失败消息,例如:

In the sequence '1, 2, 3', the 1st element is failing: 1.0 is not close to 2.0 +/- 0.5

这应该几乎可以开箱即用。缺少的部分是从 A => 的隐式转换。 MatchResult[_]Matcher[A] (我将添加到下一个版本中):

implicit def functionResultToMatcher[T](f: T => MatchResult[_]): Matcher[T] = (t: T) => {
  val result = f(t)
  (result.isSuccess, result.message)
}

您可以使用 foreach 而不是forall 如果你想获得所有失败:

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

这每天都会变得更好。使用最新的specs2快照,您可以编写:

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)
}   

更新3

现在,使用最新的specs2快照,您可以编写:

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)
}   

失败消息将是:

In the sequence '1, 2, 3', the 1st element is failing: the value '1.0' is not close to 2.0 +/- 0.5

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:

  1. f is an Expectable[A => Double] so we need to take its actual value to be able to use it

  2. 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:

  1. the inherited result method building a MatchResult (what the apply method of any Matcher should return

  2. passing it a boolean saying if the execution of beCloseTo was successful: .isSuccess

  3. passing it nicely formatted "ok" and "ko" messages, based on the input and on the result message of the beCloseTo matching

  4. 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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文