Last active
August 29, 2015 14:12
-
-
Save bvenners/aa9a78afb91e25795d01 to your computer and use it in GitHub Desktop.
An example of matcher composition in ScalaTest
This file contains hidden or 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
// | |
// 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