Last active
July 28, 2016 12:56
-
-
Save ittaiz/c775c382d6b53f74b6e3cd53d9169127 to your computer and use it in GitHub Desktop.
Polymorphic matching
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | |
} | |
} |
Actually given they have the beLike I don't know if the contribution is that dramatic
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")
})
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.
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
Contributing to spec2:
Yeah, why not :) Just I personally would prefer syntax like this
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 :)