Создание соответствия Specs2 по модульному принципу - PullRequest
8 голосов
/ 27 октября 2011

У меня есть функции A => Double. Я хочу проверить, дают ли две такие функции одинаковые результаты (с точностью до допуска, используя существующее сопоставление 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))

Это намного короче, чем решение Эрика, но не дает хорошего сообщения об ошибке. То, что я хотел бы иметь возможность переименовать сопоставленное значение во втором методе. Что-то вроде следующего (которое не компилируется).

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)

1 Ответ

9 голосов
/ 28 октября 2011

Если вы хотите писать вещи со второй версией, вам нужно создать новый класс 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)          
}

Теперь давайте посмотрим, как повторно использовать сопоставитель beCloseTo в методе apply:

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 к последовательности значений :

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

Обратите внимание, что:

  1. f - это Expectable[A => Double], поэтому нам нужно взять его фактическое value, чтобы иметь возможность использовать его

  2. аналогично, мы можем применить Expectable[T] только к Matcher[T], поэтому нам нужно использовать метод theValue для преобразования f.value(v) в Expectable[Double] (из черты MustExpectations)

Наконец, когда у нас есть результат совпадения forall, мы можем настроить сообщения о результатах, используя:

  1. унаследованный result метод, строящий MatchResult (что должен возвращать apply метод любого Matcher

  2. передавая логическое выражение, если выполнение beCloseTo было успешным: .isSuccess

  3. передача его в хорошо отформатированных сообщениях "ok" и "ko" на основе входных данных и сообщения результата beCloseTo, соответствующего

  4. передавая ему Expectable, который использовался для сопоставления, в первую очередь: f, так что конечный результат имеет тип MatchResult[A => Double]

Я не уверен, насколько модульными мы можем стать, учитывая ваши требования. Мне кажется, что лучшее, что мы можем здесь сделать, это повторно использовать beCloseTo с forall.

UPDATE

Более короткий ответ может быть примерно таким:

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

ОБНОВЛЕНИЕ 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
...