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

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