Een Specs2-matcher op een modulaire manier maken

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.

Ik wil kunnen schrijven:

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

Ik wil deze matcher op een modulaire manier bouwen en niet alleen maar een Matcher [TF] schrijven.

Het zou nog leuker zijn als ik kon schrijven:

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

Ik wil ook een redelijke beschrijving krijgen wanneer de matcher faalt.

Bewerk

Na eroverheen te hebben geslapen kwam ik met het volgende.

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

Dit is veel korter dan de oplossing van Eric, maar biedt geen goede foutmelding. Wat ik graag zou willen is de naam van de toegewezen waarde in de tweede methode. Iets als het volgende (dat niet compileert).

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

1 antwoord

Als u dingen wilt schrijven met de tweede versie, moet u een nieuwe Matcher -klasse maken met de functionaliteit van de beCloseTo -overeenkomst:

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

Met deze klasse kunt u de syntaxis gebruiken die u zoekt:

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

Laten we nu kijken hoe we de beCloseTo -overeenkomst in de apply -methode opnieuw kunnen gebruiken:

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 de bovenstaande code passen we een functie toe die een MatcherResult naar een reeks waarden :

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

Let daar op:

  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)

Als we uiteindelijk het resultaat van de forall -overeenkomst hebben, kunnen we de resultaatberichten aanpassen met behulp van:

  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]

Ik weet niet zeker hoe modulairer we aan uw eisen kunnen komen. Volgens mij is het beste wat we hier kunnen doen, het opnieuw gebruiken van beCloseTo met forall .

UPDATE

Een korter antwoord kan zoiets als dit zijn:

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

De bovenstaande code creëert een foutmelding zoals:

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

U kunt gebruiken foreach in plaats van forall als u alle fouten wilt ontvangen:

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

Dit wordt elke dag beter. Met de nieuwste momentopname van specs2 kunt u schrijven:

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

En nu met de nieuwste momentopname van specs2 kunt u schrijven:

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

Het foutbericht zal zijn:

In the sequence '1, 2, 3', the 1st element is failing: the value '1.0' is not close to 2.0 +/- 0.5
9
toegevoegd
Bedankt voor je antwoord Eric. Uw suggestie werkt, maar schrijft in feite een nieuwe matcher. Ik heb aan mijn vraag een kortere manier toegevoegd om de matcher te maken (meer zoals ik dacht), maar het ontbreekt aan de juiste berichten.
toegevoegd de auteur ziggystar, de bron
Elke kans om de functie toe te voegen om een ​​ ^^ transformatie te versieren met zoiets als een alias? Immers wanneer u een ^^ (A => B) heeft, moet u uitleggen hoe u de B hebt verkregen uit de A , zoals " het eerste element van 'als ^^ (_.head) voor een deel wordt gedaan.
toegevoegd de auteur ziggystar, de bron