The specs2 matcher system provides a comprehensive set of type-safe matchers for assertions in specifications. Matchers can be composed, negated, and customized to create expressive and readable test assertions.
Base trait for all matchers providing composition and transformation methods.
trait Matcher[T] {
def apply[S <: T](expectable: Expectable[S]): MatchResult[S]
// Composition methods
def and[S <: T](m: Matcher[S]): Matcher[S]
def or[S <: T](m: Matcher[S]): Matcher[S]
def not: Matcher[T]
// Transformation methods
def ^^[S](f: S => T): Matcher[S]
def when(condition: => Boolean): Matcher[T]
def unless(condition: => Boolean): Matcher[T]
def iff(condition: => Boolean): Matcher[T]
// Utility methods
def eventually: Matcher[T]
def lazily: Matcher[T]
def orSkip: Matcher[T]
def orPending: Matcher[T]
}Result of applying a matcher to a value.
case class MatchResult[T](
expectable: Expectable[T],
message: Message,
negatedMessage: Message
) {
def isSuccess: Boolean
def isFailure: Boolean
def message: String
def negatedMessage: String
}Wrapper for values being tested with additional metadata.
case class Expectable[T](
value: T,
description: String = "",
showValue: Boolean = true
) {
def must[R](m: Matcher[R]): MatchResult[T]
def should[R](m: Matcher[R]): MatchResult[T]
def aka(alias: String): Expectable[T]
def updateDescription(f: String => String): Expectable[T]
}Main trait aggregating all standard matchers.
trait Matchers extends AnyMatchers
with BeHaveMatchers
with TraversableMatchers
with MapMatchers
with StringMatchers
with NumericMatchers
with ExceptionMatchers
with OptionMatchers
with EitherMatchers
with FutureMatchers
with EventuallyMatchers
with XmlMatchers
with JsonMatchers"Must" syntax for expectations that return results.
trait MustMatchers extends Matchers {
implicit def anyToMust[T](t: T): MustExpectable[T]
}
class MustExpectable[T](value: T) {
def must[R](m: Matcher[R]): MatchResult[T]
def must(m: Matcher[T]): MatchResult[T]
}"Should" syntax for expectations.
trait ShouldMatchers extends Matchers {
implicit def anyToShould[T](t: T): ShouldExpectable[T]
}
class ShouldExpectable[T](value: T) {
def should[R](m: Matcher[R]): MatchResult[T]
def should(m: Matcher[T]): MatchResult[T]
}General-purpose matchers for any type.
trait AnyMatchers {
def beTrue: Matcher[Boolean]
def beFalse: Matcher[Boolean]
def beNull[T]: Matcher[T]
def beEqualTo[T](t: T): Matcher[T]
def be_===[T](t: T): Matcher[T]
def beOneOf[T](values: T*): Matcher[T]
def beAnInstanceOf[T: ClassTag]: Matcher[Any]
def haveClass[T: ClassTag]: Matcher[Any]
def haveSuperclass[T: ClassTag]: Matcher[Any]
def beAssignableFrom[T: ClassTag]: Matcher[Class[_]]
}Usage Examples:
true must beTrue
null must beNull
5 must beEqualTo(5)
"test" must beOneOf("test", "spec", "example")
List(1,2,3) must beAnInstanceOf[List[Int]]String-specific matchers.
trait StringMatchers {
def contain(s: String): Matcher[String]
def startWith(s: String): Matcher[String]
def endWith(s: String): Matcher[String]
def beMatching(regex: String): Matcher[String]
def beMatching(regex: Regex): Matcher[String]
def find(regex: String): Matcher[String]
def have size(n: Int): Matcher[String]
def have length(n: Int): Matcher[String]
def be empty: Matcher[String]
def beBlank: Matcher[String]
}Usage Examples:
"hello world" must contain("world")
"specs2" must startWith("spec")
"testing" must endWith("ing")
"abc123" must beMatching("\\w+\\d+")
"" must be empty
" " must beBlankMatchers for collections and traversable types.
trait TraversableMatchers {
def contain[T](t: T): Matcher[Traversable[T]]
def containMatch[T](regex: String): Matcher[Traversable[T]]
def containPattern[T](regex: String): Matcher[Traversable[T]]
def haveSize[T](n: Int): Matcher[Traversable[T]]
def haveLength[T](n: Int): Matcher[Traversable[T]]
def be empty[T]: Matcher[Traversable[T]]
def beSorted[T: Ordering]: Matcher[Traversable[T]]
def containAllOf[T](elements: T*): Matcher[Traversable[T]]
def containAnyOf[T](elements: T*): Matcher[Traversable[T]]
def atLeastOnce[T](m: Matcher[T]): Matcher[Traversable[T]]
def atMostOnce[T](m: Matcher[T]): Matcher[Traversable[T]]
def exactly[T](n: Int, m: Matcher[T]): Matcher[Traversable[T]]
}Usage Examples:
List(1, 2, 3) must contain(2)
List(1, 2, 3) must haveSize(3)
List.empty[Int] must be empty
List(1, 2, 3) must beSorted
List(1, 2, 3, 2) must containAllOf(1, 2)
List("a", "b", "c") must atLeastOnce(startWith("a"))Numeric comparison matchers.
trait NumericMatchers {
def beLessThan[T: Ordering](n: T): Matcher[T]
def beLessThanOrEqualTo[T: Ordering](n: T): Matcher[T]
def beGreaterThan[T: Ordering](n: T): Matcher[T]
def beGreaterThanOrEqualTo[T: Ordering](n: T): Matcher[T]
def beBetween[T: Ordering](min: T, max: T): Matcher[T]
def beCloseTo[T: Numeric](expected: T, delta: T): Matcher[T]
def bePositive[T: Numeric]: Matcher[T]
def beNegative[T: Numeric]: Matcher[T]
def beZero[T: Numeric]: Matcher[T]
}Usage Examples:
5 must beLessThan(10)
10 must beGreaterThanOrEqualTo(10)
7 must beBetween(5, 10)
3.14159 must beCloseTo(3.14, 0.01)
5 must bePositive
-3 must beNegative
0 must beZeroMatchers for testing exceptions.
trait ExceptionMatchers {
def throwA[E <: Throwable: ClassTag]: Matcher[Any]
def throwAn[E <: Throwable: ClassTag]: Matcher[Any]
def throwA[E <: Throwable: ClassTag](message: String): Matcher[Any]
def throwA[E <: Throwable: ClassTag](messagePattern: Regex): Matcher[Any]
def throwA[E <: Throwable: ClassTag](matcher: Matcher[E]): Matcher[Any]
}Usage Examples:
{ throw new IllegalArgumentException("bad arg") } must throwAn[IllegalArgumentException]
{ 1 / 0 } must throwA[ArithmeticException]
{ validate("") } must throwA[ValidationException]("empty input")
{ parse("invalid") } must throwA[ParseException](beMatching(".*invalid.*"))Matchers for Option types.
trait OptionMatchers {
def beSome[T]: Matcher[Option[T]]
def beSome[T](t: T): Matcher[Option[T]]
def beSome[T](matcher: Matcher[T]): Matcher[Option[T]]
def beNone[T]: Matcher[Option[T]]
}Usage Examples:
Some(5) must beSome
Some("test") must beSome("test")
Some(10) must beSome(beGreaterThan(5))
None must beNoneMatchers for Either types.
trait EitherMatchers {
def beRight[T]: Matcher[Either[_, T]]
def beRight[T](t: T): Matcher[Either[_, T]]
def beRight[T](matcher: Matcher[T]): Matcher[Either[_, T]]
def beLeft[T]: Matcher[Either[T, _]]
def beLeft[T](t: T): Matcher[Either[T, _]]
def beLeft[T](matcher: Matcher[T]): Matcher[Either[T, _]]
}Usage Examples:
Right(42) must beRight
Right("success") must beRight("success")
Left("error") must beLeft
Left(404) must beLeft(beGreaterThan(400))Matchers for asynchronous Future types.
trait FutureMatchers {
def await[T]: Matcher[Future[T]]
def await[T](duration: Duration): Matcher[Future[T]]
def beEqualTo[T](t: T): FutureMatcher[T]
def throwA[E <: Throwable: ClassTag]: FutureMatcher[Any]
}
trait FutureMatcher[T] extends Matcher[Future[T]] {
def await: Matcher[Future[T]]
def await(duration: Duration): Matcher[Future[T]]
}Usage Examples:
Future(42) must beEqualTo(42).await
Future.failed(new RuntimeException) must throwA[RuntimeException].await
Future(slow()) must beEqualTo(result).await(5.seconds)Retry-based matchers for eventually consistent conditions.
trait EventuallyMatchers {
def eventually[T](m: Matcher[T]): Matcher[T]
def eventually[T](m: Matcher[T], retries: Int): Matcher[T]
def eventually[T](m: Matcher[T], sleep: Duration): Matcher[T]
def eventually[T](m: Matcher[T], retries: Int, sleep: Duration): Matcher[T]
def retry[T](m: Matcher[T]): Matcher[T]
def atMost[T](duration: Duration): RetryMatcher[T]
def atLeast[T](duration: Duration): RetryMatcher[T]
}Usage Examples:
asyncOperation() must eventually(beEqualTo(expected))
database.count() must eventually(beGreaterThan(0), retries = 10)
cache.get(key) must eventually(beSome, 100.millis)Matchers for Map types.
trait MapMatchers {
def haveKey[K](k: K): Matcher[Map[K, _]]
def haveValue[V](v: V): Matcher[Map[_, V]]
def havePair[K, V](k: K, v: V): Matcher[Map[K, V]]
def havePairs[K, V](pairs: (K, V)*): Matcher[Map[K, V]]
def haveKeys[K](keys: K*): Matcher[Map[K, _]]
def haveValues[V](values: V*): Matcher[Map[_, V]]
}Usage Examples:
Map("a" -> 1, "b" -> 2) must haveKey("a")
Map("a" -> 1, "b" -> 2) must haveValue(1)
Map("a" -> 1, "b" -> 2) must havePair("a" -> 1)
Map("a" -> 1, "b" -> 2) must haveKeys("a", "b")Combine matchers with logical operators:
// AND composition
result must (beGreaterThan(0) and beLessThan(100))
// OR composition
result must (beEqualTo("success") or beEqualTo("ok"))
// Negation
result must not(beEmpty)
result must not(contain("error"))Apply matchers conditionally:
// When condition is true
result must beEqualTo(expected).when(enableValidation)
// Unless condition is true
result must beEmpty.unless(hasData)
// If and only if condition is true
result must bePositive.iff(isEnabled)Transform values before matching:
// Transform with function
users must haveSize(3) ^^ (_.filter(_.active))
// Transform with partial function
response must beEqualTo(200) ^^ { case HttpResponse(code, _) => code }def customMatcher[T](f: T => Boolean, description: String): Matcher[T] = {
(t: T) => {
val result = f(t)
MatchResult(result, s"$t $description", s"$t does not $description")
}
}Examples:
def beValidEmail = beMatching("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b".r) ^^
((_: String).toLowerCase, "a valid email address")
def haveValidChecksum = customMatcher[File](
file => calculateChecksum(file) == expectedChecksum,
"have valid checksum"
)
"user@example.com" must beValidEmail
new File("data.txt") must haveValidChecksumBuild complex matchers from simple ones:
def beValidUser = (
have(name = not(beEmpty)) and
have(email = beValidEmail) and
have(age = beGreaterThan(0))
)
User("john", "john@test.com", 25) must beValidUserNatural language matchers using "be" and "have":
trait BeHaveMatchers {
def be(m: Matcher[Any]): Matcher[Any]
def have(m: Matcher[Any]): Matcher[Any]
}Usage:
user must be(valid)
list must have(size(3))
file must be(readable)
response must have(status(200))Test values within specific scopes:
users must contain { user: User =>
user.name must startWith("John")
user.age must beGreaterThan(18)
}Customize matcher failure messages:
def bePositive = be_>=(0) ^^ ((_: Int), "a positive number")
(-5) must bePositive
// Failure: -5 is not a positive numberand/or for complex conditionseventually and await for asynchronous operations