Skip to content

Instantly share code, notes, and snippets.

@ittaiz
Last active July 28, 2016 12:56
Show Gist options
  • Save ittaiz/c775c382d6b53f74b6e3cd53d9169127 to your computer and use it in GitHub Desktop.
Save ittaiz/c775c382d6b53f74b6e3cd53d9169127 to your computer and use it in GitHub Desktop.
Polymorphic matching
class FruitTest extends SpecificationWithJUnit {
trait Fruit
case class Apple(color: String) extends Fruit
case class Peach(ripeness: Double) extends Fruit
def aColorfulApple(color: String): Matcher[Fruit] = {
val appleMatcher: Matcher[Apple] = be_===(color) ^^ { a: Apple =>
a.color aka "apple color"
}
val fruitMatcher: Matcher[Fruit] = (_: Fruit) match {
case apple: Apple => appleMatcher.apply(MustExpectable[Apple](apple))
case _ => ko("something which is not an apple")
}
fruitMatcher
}
"match an apple from a set of fruits" in {
Set(Apple("red"), Apple("green"), Peach(0.3)) must contain(aColorfulApple("green"))
}
}
@povilas
Copy link

povilas commented Jul 28, 2016

I'm using probably a little bit more generic version of this. What I like more about this version is that aColorfulApple has type of Matcher[Apple] and not Matcher[Fruit]

import org.specs2.matcher.{MatchFailure, Matcher, Matchers}
import org.specs2.mutable.SpecWithJUnit

import scala.reflect._

class FruitTest extends SpecWithJUnit with Matchers {

  trait Fruit

  case class Apple(color: String) extends Fruit

  case class Peach(ripeness: Double) extends Fruit

  def isA[A, T: ClassTag](matcher: Matcher[T]): Matcher[A] = beLike[A] {
    case value: T => matcher(value)
    case value => MatchFailure("", "is not " + classTag[T].runtimeClass.getSimpleName, value)
  }

  def aColorfulApple(color: String): Matcher[Apple] =
    be_===(color) ^^ ((_: Apple).color aka "color")

  "match an apple from a set of fruits" in {
    Set(Apple("red"), Apple("green"), Peach(0.3)) must contain(isA(aColorfulApple("green")))
  }

}

@ittaiz
Copy link
Author

ittaiz commented Jul 28, 2016

I thought about this version but thought it's a bit too much for my use-case. Maybe not.
Don't you think you want to say something about T being a subtype of A?

@ittaiz
Copy link
Author

ittaiz commented Jul 28, 2016

but what I like about your solution a lot (and I didn't know how to do) is the catch all case where you get the value and extract the runtime class out of it, well done!
It is equivalent to catch _ with respect to match error, right?

@povilas
Copy link

povilas commented Jul 28, 2016

yup

@povilas
Copy link

povilas commented Jul 28, 2016

because we have a lot of polymorphic collections (event streams.. duh), so it's nicer to have event matchers less abstract and to have isA as generic kind of "adapter".

and if have def aColorfulApple(color: String): Matcher[Fruit], it can be akward to re-use on concrete values (not in collections), i.e. this compiles:

Peach(0.3) mustEqual aColorfulApple("green")
Apple("red") mustEqual aColorfulApple("green")

@ittaiz
Copy link
Author

ittaiz commented Jul 28, 2016

definitely. WDYT about contributing it to specs2? And WDYT about the T subtype of A?

@povilas
Copy link

povilas commented Jul 28, 2016

Contributing to spec2:
Yeah, why not :) Just I personally would prefer syntax like this

class FruitTest extends SpecWithJUnit with Matchers {

  trait Fruit

  case class Apple(color: String) extends Fruit

  case class Peach(ripeness: Double) extends Fruit

  implicit class IsA[T: ClassTag](matcher: Matcher[T]) {
    def unary_~[A] = beLike[A] {
      case value: T => matcher(value)
      case value => MatchFailure("", "is not " + classTag[T].runtimeClass.getSimpleName, value)
    }
  }

  def aColorfulApple(color: String): Matcher[Apple] =
    be_===(color) ^^ ((_: Apple).color aka "color")

  "match an apple from a set of fruits" in {
    Set(Apple("red"), Apple("green"), Peach(0.3)) must contain(~aColorfulApple("greenz"))
  }
}

But not everyone likes this :)

T subtype of A:
If you thinking about having def isA[A <: T, T: ClassTag](matcher: Matcher[T]): Matcher[A] - it doesn't compile and i'm not sure how to make it work :)

Error:(25, 67) type mismatch;
 found   : org.specs2.matcher.ValueCheck[FruitTest.this.Apple]
 required: org.specs2.matcher.ValueCheck[Product with Serializable with FruitTest.this.Fruit]
Note: FruitTest.this.Apple <: Product with Serializable with FruitTest.this.Fruit, but trait ValueCheck is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
    Set(Apple("red"), Apple("green"), Peach(0.3)) must contain(isA(aColorfulApple("greenz")))

@ittaiz
Copy link
Author

ittaiz commented Jul 28, 2016

Actually given they have the beLike I don't know if the contribution is that dramatic

@povilas
Copy link

povilas commented Jul 28, 2016

yup, its just small wrapper for simple:

    Set(Apple("red"), Apple("green"), Peach(0.3)) must contain(beLike[Fruit] {
      case value: Apple => aColorfulApple("greenz").apply(value)
      case _ => ko("not apple")
    })

@holograph
Copy link

I actually have a very different way of implementing this:

sealed trait Category
case class SpecificCategory(term: String) extends Category
case class CategoryGroup(prefix: String) extends Category

trait SpecificCategoryMatchers {
  // Basic category matcher:
  def aSpecificCategory: Matcher[SpecificCategory] =
    beAnInstanceOf[SpecificCategory]

  // Matcher builder as an extended class
  implicit class ExtendSpecificCategoryMatcher(base: Matcher[SpecificCategory]) {
    def withTerm(termMatcher: Matcher[String]): Matcher[SpecificCategory] =
      base and termMatcher ^^ { sc: SpecificCategory => sc.term }
    // ... more builders
  }

  // Two implicit conversions for matching against containers (this is where the subtyping relationship really matters)
  /** Enables syntax like `results must contain(aSpecificCategory(...))` */
  implicit def `SpecificCategory matcher as ValueCheck`(m: Matcher[SpecificCategory]): ValueCheck[Category] =
    matcherIsValueCheck[Category](
      beAnInstanceOf[SpecificCategory] and
      { result: Category => result.asInstanceOf[SpecificCategory] } ^^ m
    )

  /** Enable syntax like `results must contain(aSpecificCategory(...), aSpecificCategory(...), ...)` */
  implicit def `Sequence of SpecificCategory matcher as sequence of ValueChecks`(seq: Seq[Matcher[SpecificCategory]]): Seq[ValueCheck[Category]] =
    seq map `SpecificCategory matcher as ValueCheck`
}

While this is a bit annoying to write (although you can copy-paste or even template most of this), it allows very flexible syntax on the usage site:

    "employ case-insensitive alphabetic ordering" in {
      val lowerCase = aCategory("lower")
      val upperCase = aCategory("Upper")
      assert(lowerCase.term > upperCase.term, "This test case requires a lexicographically-ordered pair")
      val index = allocateIndex(aPayload.withCategories(lowerCase, upperCase))
      val results = index.suggestCategory("er").results
      // This is where the magic happens: results is a Seq[Category] and we're matching on multiple entries
      results should contain(exactly(
        aSpecificCategory.withTerm("lower"),
        aSpecificCategory.withTerm("upper")
      ))
    }

The same principal applies to all of the other subtypes, and you can mix-and-match them safely.

@holograph
Copy link

The whole thing can be found in the onboarding server: matchers and test spec.

@povilas
Copy link

povilas commented Jul 28, 2016

yup, this really looks nice in tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment