Skip to content

Instantly share code, notes, and snippets.

@bvenners
Last active August 29, 2015 14:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bvenners/aa9a78afb91e25795d01 to your computer and use it in GitHub Desktop.
Save bvenners/aa9a78afb91e25795d01 to your computer and use it in GitHub Desktop.
An example of matcher composition in ScalaTest
//
// TLDR: In ScalaTest, matcher composition is simply function composition.
//
//
// The beOdd matcher in the documentation for org.scalatest.matchers.Matcher is simple, but it will
// concatenate strings that are not needed. If you want to avoid that as a (premature) performance
// optimization, you can write it like this:
//
scala> val beOdd =
| Matcher { (left: Int) =>
| MatchResult(
| left % 2 == 1,
| "{0} was not odd",
| "{0} was odd",
| Vector(left),
| Vector(left)
| )
| }
beOdd: org.scalatest.matchers.Matcher[Int] = Matcher[int](int => MatchResult)
//
// All of ScalaTest's matchers produce MatchResults like that, to avoid doing any string
// concatenation until and unless it is actually needed. This was a "post-mature" optimization,
// as we measured up to 10% of runtime in ScalaTest 1.x tests that used matchers was concatenating
// strings, most of which weren't needed.
//
// Here are some examples of beOdd in use:
//
scala> 1 should beOdd
scala> 1 shouldNot beOdd
org.scalatest.exceptions.TestFailedException: 1 was odd
at org.scalatest.MatchersHelper$.newTestFailedException(MatchersHelper.scala:160)
at org.scalatest.Matchers$ShouldMethodHelper$.shouldNotMatcher(Matchers.scala:6237)
at org.scalatest.Matchers$AnyShouldWrapper.shouldNot(Matchers.scala:6624)
... 43 elided
scala> 2 should beOdd
org.scalatest.exceptions.TestFailedException: 2 was not odd
at org.scalatest.MatchersHelper$.newTestFailedException(MatchersHelper.scala:160)
at org.scalatest.Matchers$ShouldMethodHelper$.shouldMatcher(Matchers.scala:6231)
at org.scalatest.Matchers$AnyShouldWrapper.should(Matchers.scala:6265)
... 43 elided
scala> 2 shouldNot beOdd
//
// OK, given that you can create a matcher against Seqs that checks for an odd length like this:
//
scala> val haveOddLength = beOdd compose { (seq: Seq[_]) => seq.length }
haveOddLength: org.scalatest.matchers.Matcher[Seq[_]] = <function1>
//
// In ScalaTest, matchers are functions, and matcher composition is just function composition. The
// beOdd matcher is a Matcher[Int], which is a Int => MatchResult function. To transform that
// Int => MatchResult into a Matcher[Seq[_]] that checks for odd lengths, you need to transform
// the Int => MatchResult function into a Seq[_] => MatchResult function. That's what the compose
// method on Function1 does. This same method is inherited by Matcher because it extends Function1, but
// Matcher overrides it to produce a more specific result type of another Matcher. Thus the result
// of passing the above Seq[_] => Int function to compose on beOdd is not just a Seq[_] => MatchResult,
// but more specifically, a Matcher[Seq[_]]. Given it is a Matcher[Seq[_]], you can use it with should:
//
scala> List(1, 2, 3) should haveOddLength
scala> List(1, 2, 3, 4) should haveOddLength
org.scalatest.exceptions.TestFailedException: 4 was not odd
at org.scalatest.MatchersHelper$.newTestFailedException(MatchersHelper.scala:160)
at org.scalatest.Matchers$ShouldMethodHelper$.shouldMatcher(Matchers.scala:6231)
at org.scalatest.Matchers$AnyShouldWrapper.should(Matchers.scala:6265)
... 43 elided
scala> List(1, 2, 3, 4) shouldNot haveOddLength
scala> List(1, 2, 3) shouldNot haveOddLength
org.scalatest.exceptions.TestFailedException: 3 was odd
at org.scalatest.MatchersHelper$.newTestFailedException(MatchersHelper.scala:160)
at org.scalatest.Matchers$ShouldMethodHelper$.shouldNotMatcher(Matchers.scala:6237)
at org.scalatest.Matchers$AnyShouldWrapper.shouldNot(Matchers.scala:6624)
... 43 elided
//
// One thing that could be improved here, perhaps, is the error message. You can use
// mapResult for that, which allows you to transform the MatchResult coming out of beOdd
// lazily, again avoiding any unnecessary string concatenation. Here's an example:
//
scala> val haveOddLength = beOdd compose { (seq: Seq[_]) => seq.length } mapResult { mr =>
| val len = mr.failureMessageArgs(0)
| MatchResult(
| mr.matches,
| s"{0} was not an odd length",
| s"{0} was an odd length",
| Vector(len),
| Vector(len)
| )
| }
haveOddLength: org.scalatest.matchers.Matcher[Seq[_]] = <function1>
//
// Given this implementation of haveOddLength, you now get a clearer error message:
//
scala> List(1, 2, 3) should haveOddLength
scala> List(1, 2, 3, 4) should haveOddLength
org.scalatest.exceptions.TestFailedException: 4 was not an odd length
at org.scalatest.MatchersHelper$.newTestFailedException(MatchersHelper.scala:160)
at org.scalatest.Matchers$ShouldMethodHelper$.shouldMatcher(Matchers.scala:6231)
at org.scalatest.Matchers$AnyShouldWrapper.should(Matchers.scala:6265)
... 43 elided
scala> List(1, 2, 3, 4) shouldNot haveOddLength
scala> List(1, 2, 3) shouldNot haveOddLength
org.scalatest.exceptions.TestFailedException: 3 was an odd length
at org.scalatest.MatchersHelper$.newTestFailedException(MatchersHelper.scala:160)
at org.scalatest.Matchers$ShouldMethodHelper$.shouldNotMatcher(Matchers.scala:6237)
at org.scalatest.Matchers$AnyShouldWrapper.shouldNot(Matchers.scala:6624)
... 43 elided
//
// This haveOddLength only works with Seqs. A more general construct would work with any type T
// for which an implicit Length[T] exists. This would require a MatcherFactory1[Any, Length], which
// is a bit more involved, but still purely functional and composable. I can show an example of
// that if you're interested.
//
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment