The most frequent way to specify some expected behaviour with specs2 is to use matchers. You generally execute an action, a command or a function and then check if the actual value you get is equal to an expected one (the “arrange-act-assert” paradigm).
For example, if you create a specification for an object manipulating paths:
// describe the functionality
s2"the directoryPath method should return well-formed paths $e1"
// give an example with some code
def e1 = Paths.directoryPath("/tmp/path/to/dir") must beEqualTo("/tmp/path/to/dir/")
The must operator takes the actual value returned by directoryPath and applies it to a Matcher built with the expected value. beEqualTo is one of the many matchers defined by specs2, it just checks if 2 values are equal.
In the following sections you will learn:
the different ways of checking the equality of values
to use the matchers for the most common data types in Scala, and most notably Traversables
to use other types of matchers in less common situations: json, xml, files, parsers combinators…
The most common type of matcher is beEqualTo to test the equality of 2 values with the underlying == operator. Several syntaxes can be used, according to your own taste
Matcher
Comment
1 must beEqualTo(1)
the normal way
1 must be_==(1)
with a symbol
1 must_== 1
my favorite!
1 mustEqual 1
if you dislike underscores
1 should_== 1
for should lovers
1 === 1
the ultimate shortcut
1 must be equalTo(1)
with a literate style
There are also other notions of equality
Matcher
Comment
beTypedEqualTo
typed equality. a must beTypedEqualTo(b) will not work if a and b don’t have compatible types
be_===
synonym for beTypedEqualTo
a ==== b
synonym for a must beTypedEqualTo(b)
a must_=== b
similar to a must_== b but will not typecheck if a and b don’t have the same type
be_==~
check if (a: A) == conv(b: B) when there is an implicit conversion conv from B to A
beTheSameAs
reference equality: check if a eq b (a must be(b) also works)
be
a must be(b): synonym for beTheSameAs
beTrue, beFalse
shortcuts for Boolean equality
beLike
partial equality, using a PartialFunction[T, MatchResult[_]]: (1, 2) must beLike { case (1, _) => ok }
Note: the beEqualTo matcher is using the regular == Scala equality. However in the case of Arrays, Scala == is just using reference equality, eq. So the beEqualTo matcher has been adapted to use the .deep method on Arrays, transforming them to IndexedSeqs (possibly nested), before checking for equality, so that Array(1, 2, 3) === Array(1, 2, 3) (despite the fact that Array(1, 2, 3) != Array(1, 2, 3)).
Now let’s check the other matchers.
Out of the box
These are the all the available matchers when you extend Specification
Optional
Those matchers are optional. To use them, you need to add a new trait to your specification.
Those are additional “data” matchers
Optional data matchers
There are several matchers to check scalaz.Either instances:
be_\/- checks if an element is Right(_)
be_\/-(exp) checks if an element is `Right(exp)
be_\/-(matcher) checks if an element is Right(a) where a satisfies the matcher
be_\/-(function: A => AsResult[B]) checks if an element is Right(a) where function(a) returns a successful Result (note that a Seq[A] is also a function Int => A so if you want to check that a sequence is contained in Right you need to use a matcher: be_\/-(===(Seq(1)))
be_\/-.like(partial function) checks if an element is Right(_) and satisfies a partial function returning a MatchResult
be_-\/ checks if an element is Left(_)
be_-\/(exp) checks if an element is Left(exp)
be_-\/(matcher) checks if an element is Left(a) where a satisfies the matcher
be_-\/(function: A => AsResult[B]) checks if an element is Left(a) where function(a) returns a successful Result (note that a Seq[A] is also a function Int => A so if you want to check that a sequence is contained in Left you need to use a matcher: be_-\/(===(Seq(1)))
be_-\/.like(partial function) checks if an element is Left(_) and satisfies a partial function returning a MatchResult
There are several matchers to check scalaz.Validation instances:
beSuccess checks if an element is Success(_)
beSuccess(exp) checks if an element is `Success(exp)
beSuccess(matcher) checks if an element is Success(a) where a satisfies the matcher
beSuccess(function: A => AsResult[B]) checks if an element is Success(a) where function(a) returns a successful Result (note that a Seq[A] is also a function Int => A so if you want to check that a sequence is contained in Success you need to use a matcher: beSuccess(===(Seq(1)))
beSuccess.like(partial function) checks if an element is Success(_) and satisfies a partial function returning a MatchResult
beFailure checks if an element is Failure(_)
beFailure(exp) checks if an element is Failure(exp)
beFailure(matcher) checks if an element is Failure(a) where a satisfies the matcher
beFailure(function: A => AsResult[B]) checks if an element is Failure(a) where function(a) returns a successful Result (note that a Seq[A] is also a function Int => A so if you want to check that a sequence is contained in Failure you need to use a matcher: beFailure(===(Seq(1)))
beFailure.like(partial function) checks if an element is Failure(_) and satisfies a partial function returning a MatchResult
There are several matchers to check cats.Validated instances:
beValid checks if an element is Valid(_)
beValid(exp) checks if an element is Valid(exp)
beValid(matcher) checks if an element is Valid(a) where a satisfies the matcher
beValid(function: A => AsResult[B]) checks if an element is Valid(a) where function(a) returns a successful Result (note that a Seq[A] is also a function Int => A so if you want to check that a sequence is contained in Valid you need to use a matcher: beValid(===(Seq(1)))
beValid.like(partial function) checks if an element is Valid(_) and satisfies a partial function returning a MatchResult
beInvalid checks if an element is Invalid(_)
beInvalid(exp) checks if an element is Invalid(exp)
beInvalid(matcher) checks if an element is Invalid(a) where a satisfies the matcher
beInvalid(function: A => AsResult[B]) checks if an element is Invalid(a) where function(a) returns a successful Result (note that a Seq[A] is also a function Int => A so if you want to check that a sequence is contained in Invalid you need to use a matcher: beInvalid(===(Seq(1)))
beInvalid.like(partial function) checks if an element is Invalid(_) and satisfies a partial function returning a MatchResult
There are several matchers to check scalaz.concurrent.Task instances. To use them, you need to include the specs2-scalaz jar in your dependencies and mix in the TaskMatchers trait to your specification.
Then you can use:
returnOk to check if a Task completes successfully
returnBefore(duration) to check if a Task completes successfully before the specified duration
returnValue(matcher) to check if a Task successfully returns a value that satisfies the matcher
returnValue(function: A => AsResult[B]) to check if a Task completes successfully with a value a, where function(a) returns a successful Result. This can be used to do conditional assertions that depend on which value is returned by your Task
failWith[ExceptionType] to check if a Task fails with an Exception of the given type
You need to add the specs2-shapeless module to your project dependencies and add the org.specs2.shapeless.Projection._ import to your file.
Then you can "project" a type A on a "smaller" type B:
import org.specs2.shapeless.Projection._
case class User(id: Int, name: String, age: Int)
case class ExpectedUser(name: String, age: Int)
val u = User(123, "Martin", 58)
u.projectOn[ExpectedUser] must_== ExpectedUser("Martin", 58)
You to add the specs2-matcher-extra module to your project dependencies and add the org.specs2.matcher.MatcherMacros trait to your specification.
Then, with the matchA matcher you can check the values of case class attributes:
// case class for a Cat
case class Cat(name: String = "", age: Int = 0, kitten: Seq[Cat] = Seq())
// a given cat
val cat = Cat(name = "Kitty", age = 6, kitten = Seq(Cat("Oreo", 1), Cat("Ella", 2)))
// this cat must be a Cat
cat must matchA[Cat]
// check the value of "name"
cat must matchA[Cat].name("Kitty")
// check the value of "age" using a matcher
def is[A](a: A) = be_==(a)
cat must matchA[Cat].age(is(6))
// check the value of "kitten" using a function returning a Result
cat must matchA[Cat].kitten((_:Seq[Cat]) must haveSize(2))
// matchers can be chained
cat must matchA[Cat]
.name("Kitty")
.age(is(6))
.kitten((_:Seq[Cat]) must haveSize(2))
That's only if you want to check the result of other matchers!
// you need to extend the ResultMatchers trait
class MatchersSpec extends Specification with matcher.ResultMatchers { def is =
"beMatching is using a regexp" ! {
("Hello" must beMatching("h.*")) must beSuccessful
}
}
Sometimes you just want to specify that a block of code is going to terminate. The org.specs2.matcher.TerminationMatchers trait is here to help. If you mix in that trait, you can write:
Thread.sleep(100) must terminate
// the default is retries = 0, sleep = 100.millis
Thread.sleep(100) must terminate(retries = 1, sleep = 60.millis)
Note that the behaviour of this matcher is a bit different from the eventually operator. In this case, we let the current Thread sleep during the given sleep time and then we check if the computation is finished, then, we retry for the given number of retries.
In a further scenario, we might want to check that triggering another action is able to unblock the first one:
action1 must terminate.when(action2)
action1 must terminate.when("starting the second action", action2)
action1 must terminate(retries=3, sleep=100.millis).when(action2)
When a second action is specified like that, action1 will be started and action2 will be started on the first retry. Otherwise, if you want to specify that action1 can only terminate when action2 is started, you write:
The terminate matcher needs an implicit ExecutionEnv to be used. See the page to learn how to get one.
Those matchers can be used to check “content”
Optional content matchers
It is very useful to have literal Xml in Scala, it is even more useful to have matchers for it! If you want to use those matchers you need to extend the org.specs2.matcher.XmlMatchers trait:
beEqualToIgnoringSpace compares 2 Nodes, without considering spaces <a><b/></a> must ==/(<a> <b/></a>) <a><b/></a> must beEqualToIgnoringSpace(<a> <b/></a>)
beEqualToIgnoringSpace can also do an ordered comparison <a><c/> <b/></a> must ==/(<a> <c/><b/></a>).ordered
on the other hand beEqualToIgnoringSpace will not check attributes order <n a="1" b="2"/> must ==/(<n b="2" a="1"/>)
the \\ matcher is an XPath-like matcher matching if a node is a direct child of another <a><b/></a> must \\\\("b")
You can also check attribute names <a><b name="value"></b></a> must \\("b", "name")
And attribute names and values as well (values are checked using a regular expression, use the quote method if you want an exact match) <a><b n="v" n2="v2" n3="v3"></b></a> must \\("b", "n"->"v", "n2"->"v\\d")
Or the content of a Text node <a>hello</a> must \\("a") \\> "hello" (alias textIs) <a>hello</a> must \\("a") \\>~ "h.*" (alias textMatches)
The equivalent of \\ for a "deep" match is simply \\\\ <a><s><c></c></s></a> must \\\\("c")
Json is a simple data format essentially modeling recursive key-values. You can use the following matchers provided by the org.specs2.matcher.JsonMatchers trait to check JSON strings:
/(value) checks if a value is present at the root of the document. This can only be the case if that document is an Array
/(regex) checks if a value matching the regex is present at the root of the document. This can only be the case if that document is an Array
/(key -> value) checks if a pair is present at the root of the document. This can only be the case if that document is a Map
*/(value) checks if a value is present anywhere in the document, either as an entry in an Array, or as the value for a key in a Map
*/(key -> value) checks if a pair is present anywhere in a Map of the document
/#(i) selects the ith element in a 0-based indexed Array or a Map and allow further checks on that element
Now the interesting part comes from the fact that those matchers can be chained to search specific paths in the Json document. For example, for the following document:
// taken from an example in the Lift project
val person = """{
"person": {
"name": "Joe",
"age": 35,
"spouse": {
"person": {
"name": "Marilyn",
"age": 33
}
}
}
}
"""
You can use these combinations:
person must /("person") */("person") /("age" -> 33.0) // by default numbers are parsed as Doubles
You can as well use regular expressions or String matchers instead of values to verify the presence of keys or elements. For example:
person must /("p.*".r) */ ".*on".r /("age" -> "\\d+\\.\\d".r)
person must /("p.*".r) */ ".*on".r /("age" -> startWith("3"))
person must /("p.*".r) */ ".*on".r /("age" -> (be_>(30) ^^ ((_:String).toInt)))
You can also access some records by their index:
person must /("person") /# 2 / "person"
Finally you can use Json matchers to match elements in an array:
The andHave method accepts any Matcher[JsonType] where JsonType is either JsonArray, JsonMap, JsonNumber, JsonString, JsonNull. In the example above we pass directly shirt and 10 as Matcher[JsonType] because there are implicit conversions from Int, Boolean, Double, String and Traversable[String] matchers (like allOf) to a Matcher[JsonType].
The Java api for files is more or less mimicked as matchers which can operate on strings denoting paths or on Files (with the org.specs2.matcher.FileMatchers trait)
beEqualToIgnoringSep check if 2 paths are the same regardless of their separators "c:\temp\hello" must beEqualToIgnoringSep("c:/temp/hello")
beAnExistingPath check if a path exists
beAReadablePath check if a path is readable
beAWritablePath check if a path is writable
beAnAbsolutePath check if a path is absolute
beAHiddenPath check if a path is hidden
beAFilePath check if a path is a file
beADirectoryPath check if a path is a directory
havePathName check if a path has a given name
haveAsAbsolutePath check if a path has a given absolute path
haveAsCanonicalPath check if a path has a given canonical path
haveParentPath check if a path has a given parent path
listPaths check if a path has a given list of children
exist check if a file exists
beReadable check if a file is readable
beWritable check if a file is writable
beAbsolute check if a file is absolute
beHidden check if a file is hidden
beAFile check if a file is a file
beADirectory check if a file is a directory
haveName check if a file has a given name
haveAbsolutePath check if a file has a given absolute path
haveCanonicalPath check if afile has a given canonical path
haveParent check if a file has a given parent path
haveList check if a file has a given list of children
The matchers from the org.specs2.matcher.ContentMatchers trait can help us check the contents of files. For example we can check that 2 text files have the same lines:
(file1, file2) must haveSameLines
file1 must haveSameLinesAs(file2)
We can check that the content of one file is contained in another one:
file1 must containLines(file2)
If the files are binary files we can also check that they have the same MD5 hash:
(file1, file2) must haveSameMD5
file1 must haveSameMD5As(file2)
Order
It is possible to relax the constraint by requiring the equality or containment to be true regardless of the order of lines:
(file1, file2) must haveSameLines.unordered
file1 must haveSameLinesAs(file2).unordered
file1 must containLines(file2).unordered
Show differences
By default only the different lines are being shown with a bit of context (lines before and after the differences). You can however change this strategy. For example if there are too many differences, you can specify that you only want the first 10:
(file1, file2) must haveSameLines.showOnly(10.differences)
In the code above 10.differences builds a DifferenceFilter which is merely a filtering function: (lines: Seq[LineComparison]) => Seq[LineComparison]) keeping the first 10 differences. A LineComparison is the result of comparing a list of lines, either a line has been added, deleted, modified or is the same.
We can compare the contents of 2 directories. We can for example check if no files are missing and none has been added:
actualDir must haveSamePathsAs(expectedDir)
// with a file filter applied to both the actual and expected directories
actualDir must haveSamePathsAs(expectedDir).withFilter((file: File) => !file.isHidden)
Once we know that all files are present we can check their content:
// the default comparison expects that files are text files and that comparison must be done line by line
actualDir must haveSameFilesAs(expectedDir)
// with a file filter applied to both the actual and expected directories
actualDir must haveSameFilesAs(expectedDir).withFilter((file: File) => !file.isHidden)
// with a MD5 matcher for binary files
actualDir must haveSameFilesAs(expectedDir).withMatcher(haveSameMD5)
// it is also possible to only check the content of actual files when they exist in the expected directory
actualDir must haveSameFilesContentAs(expectedDir)
Files are not the only possible source of lines and it is useful to be able to check the content of a File with a Seq[String]:
file1 must haveSameLinesAs(Seq(line1, line2, line3))
This is because those 2 types implement the org.specs2.text.LinesContent trait, defining:
a name for the overall content
a method for returning the lines
a default method for computing the differences of 2 sequences of lines (in case you need to override this logic)
So if you have a specific type T which you can represent as a Seq[String], you can create an implicit LinesContent and then you'll be able to use the ContentMatchers:
implicit def linesforMyType[T]: LinesContent[T] = new LinesContent[T] {
def name(t: T) = "My list of lines"
def lines(t: T): Seq[String] = Seq()// your implementation goes here
}
And finally those matchers are Scala / Language related
extend the org.specs2.matcher.ParserMatchers trait
associate the val parsers variable with your parsers definition
use the beASuccess, beAFailure, succeedOn, failOn, errorOn matchers to specify the results of parsing input strings. beAPartialSuccess, be aPartialSuccess, succeedOn.partially will allow a successful match only on part of the input
use haveSuccessResult and haveFailureMsg to specify what happens only on success or failure. Those matchers accept a String or a matcher so that . haveSuccessResult("r") <==> haveSuccessResult(beMatching(".*r.*") ^^ ((_:Any).toString) . haveFailingMsg("m") <==> haveFailingMsg(beMatching(".*r.*"))
For example, specifying a Parser for numbers could look like this:
import NumberParsers.{error, number}
class ParserSpec extends Specification with matcher.ParserMatchers { def is = s2"""
Parsers for numbers
beASuccess and succeedOn check if the parse succeeds
${ number("1") must beASuccess }
${ number("1i") must beAPartialSuccess }
${ number must succeedOn("12") }
${ number must succeedOn("12ab").partially }
${ number must succeedOn("12").withResult(12) }
${ number must succeedOn("12").withResult(equalTo(12)) }
${ number("1") must haveSuccessResult("1") }
beAFailure and failOn check if the parse fails
${ number must failOn("abc") }
${ number must failOn("abc").withMsg("string matching regex.*expected") }
${ number must failOn("abc").withMsg(matching(".*string matching regex.*expected.*")) }
${ number("i") must beAFailure }
${ number("i") must haveFailureMsg("i' found") }
beAnError and errorOn check if the parser errors out completely
${ error must errorOn("") }
${ error("") must beAnError }
"""
val parsers = NumberParsers
}
Some behaviours can be encoded with the type system and you just want to check that a given expression will typecheck:
import org.specs2.execute._, Typecheck._
import org.specs2.matcher.TypecheckMatchers._
typecheck {
"""
// there must be a Monoid instance for Text
Monoid[Text].zero
"""
} must succeed
You might also want to check that another expression will fail to typecheck:
typecheck {
"""
// there must be not a Monoid instance for Plane
Monoid[Plane].zero
"""
} must not succeed
typecheck {
"""
// there must be not a Monoid instance for Plane
Monoid[Plane].zero
"""
} must failWith("could not find implicit value for .* scalaz.Monoid")
It is possible to tweak the behaviour of the typecheck method to allow errors to be reported at compile-time instead of runtime:
macro-expansion errors can be reported at compile-time (default behaviour is runtime)
implicit errors can be reported at compile-time (default behaviour is runtime)
parsing errors can be reported at runtime (default behaviour is compile-time)
Here is how to do it:
// to get macro errors at compile time
typecheckWith(macrosAtCompileTime)("badBadMacro")
// to get macro errors at compile time
typecheckWith(implicitsAtCompileTime)("Monoid[Plane]")
// to get parsing errors at run-time
typecheckWith(parsingAtRuntime)("#$p6")
// combine parameters
typecheckWith(macrosAtCompileTime <| implicitsAtCompileTime)("Monoid[Plane]")
Finally you can also use a string interpolator and pass parameters:
tcw" 1 must_== 1"(parsingAtRuntime)
In the rare case where you want to use the Scala interpreter and execute a script:
class ScalaInterpreterMatchersSpec extends mutable.Specification with ScalaInterpreterMatchers {
def interpret(s: String): String = "" // you have to provide your own Scala interpreter here
"A script" can {
"be interpreted" in {
"1 + 1" >| "2"
}
}
}
It is highly desirable to have acyclic dependencies between the packages of a project. This often leads to describing the packages structure as "layered": each package on a layer can only depend on a package on a lower layer. specs2 helps you enforce this design property with specific matchers.
First you need to define the packages and their expected dependencies. Mix-in the org.specs2.specification.Analysis trait and define layers (taking specs2 as an example):
layers (
"runner",
"reporter",
"specification mutable",
"mock form",
"matcher",
"execute",
"reflect xml time html",
"collection control io text main data").withPrefix("org.specs2")
The above expression defines layers as an ordered list of Strings containing space-separated package names. It is supplemented by a withPrefix declaration to factor out the common package prefix between all these packages.
By default, the packages are supposed to correspond to directories in the src/target/scala-<version>/classes directory. If your project has a different layout you can declare another target directory:
Every rule has exceptions :-). In some rare cases, it might be desirable to exclude a class from being checked on a given layer. To do this, you can use the include/exclude methods on the Layer class:
layers (
"runner",
"reporter",
"specification mutable".exclude("mutable.SpecificationWithJUnit"),
"mock form",
"matcher",
"execute",
"reflect xml time html",
"collection control io text main data").withPrefix("org.specs2")
The include/exclude methods accept a list of regular expressions to:
exclude fully qualified class names (generally, only exclude will be necessary)
re-include fully qualified class names if the exclusion list is to big
Now you've defined layers, you can use the beRespected matcher to check if all the dependencies are verified:
val design = layers("...")
design must beRespected
If some dependencies are not respected:
[error] those dependencies are not satisfied:
[error] org.specs2.main x-> org.specs2.io because org.specs2.io.FileSystem -> org.specs2.main.Arguments
[error] org.specs2.main x-> org.specs2.io because org.specs2.io.FileSystem -> org.specs2.main.ArgumentsArgs
The org.specs2.specification.Analysis trait allows to directly embed the layers definition in a Specification and turn it into an Example:
class DependenciesSpec extends Specification with specification.Analysis { def is =
"this is the application design" ^
layers(
"gui commandline",
"controller",
"backend"
)
}
Derive matchers
The easiest way to create a new matcher is to derive it from an existing one. You can:
use logical operators
def beBetween(i: Int, j: Int) = be_>=(i) and be_<=(j)
“adapt” the actual value
// This matcher adapts the existing `be_<=` matcher to a matcher applicable to `Any`
def beShort1 = be_<=(5) ^^ { (t: Any) => t.toString.length }
// you can use aka to provide some information about the original value, before adaptation
def beShort2 = be_<=(5) ^^ { (t: Any) => t.toString.length aka "the string size" }
// !!! note: use a BeTypedEqualTo matcher when using aka and equality, otherwise you will be matching against Expectable[T] !!!
def beFive = be_===(5) ^^ { (t: Any) => t.toString.length aka "the string size" }
// The adaptation can also be done the other way around when it's more readable
def haveExtension(extension: =>String) = ((_:File).getPath) ^^ endWith(extension)
adapt the actual and expected values. This matcher compares 2 Human objects but set their wealth field to 0 so that the equals method will not fail on that field:
use eventually to try a match a number of times until it succeeds:
val iterator = List(1, 2, 3).iterator
// Use eventually(retries, n.millis) to use another number of tries and waiting time
iterator.next must be_==(3).eventually
use await to create a matcher that will match on Matcher[Future[T]] (this requires an execution environment):
Future(1) must be_>(0).await
Future { Thread.sleep(100); 1 } must be_>(0).await(retries = 2, timeout = 100.millis)
use attempt to create a matcher that will match on Matcher[scalaz.concurrent.Future[T]] (this requires an execution environment):
// see the Matchers-Futures reference card on how to get an ExecutionEnv
implicit val ee: ExecutionEnv = ???
scalaz.concurrent.Future(1) must be_>(0).attempt
scalaz.concurrent.Future { Thread.sleep(100); 1 } must be_>(0).attempt(retries = 2, timeout = 100.millis)
use when or unless to apply a matcher only if a condition is satisfied:
1 must be_==(2).when(false) // will return a success
1 must be_==(2).unless(true) // same thing
1 must be_==(2).when(false, "don't check this") // will return a success
1 must be_==(2).unless(true, "don't check this") // same thing
use iff to say that a matcher must succeed if and only if a condition is satisfied:
1 must be_==(1).iff(true) // will return a success
1 must be_==(2).iff(true) // will return a failure
1 must be_==(2).iff(false) // will return a success
1 must be_==(1).iff(false) // will return a failure
use orSkip to return a Skipped result instead of a Failure if the condition is not satisfied
1 must be_==(2).orSkip
1 must be_==(2).orSkip("Precondition failed") // prints "Precondition failed: '1' is not equal to '2'"
1 must be_==(2).orSkip((ko:String) => "BAD "+ko) // prints "BAD '1' is not equal to '2'"
use orPending to return a Pending result instead of a Failure if the condition is not satisfied
1 must be_==(2).orPending
1 must be_==(2).orPending("Precondition failed") // prints "Precondition failed: '1' is not equal to '2'"
1 must be_==(2).orPending((ko:String) => "BAD "+ko) // prints "BAD '1' is not equal to '2'"
use zip operators to match each value of a tuple
type MyTuple = (String, String, String, Seq[(String, Double)])
val t1: MyTuple = ("a", "b", "c", Seq(("d", 1.01), ("e", 2.02)))
val t2: MyTuple = ("a", "b", "c", Seq(("d", 1.00), ("e", 2.00)))
// create a matcher by zipping matchers to the expected value
def beMatching(expected: MyTuple) = expected.zip(startWith, ===, ===, matchSequence)
// match the elements of a sequence with a zipped matcher using string equality for the first field and
// approximate Double equality for the second field
def matchSequence(expected: =>Seq[(String, Double)]) = expected.contain(_.zip(===, ==~)).inOrder
/** type inference doesn't work if this matcher, specialised to Double, is not defined */
def ==~(d: =>Double) = beCloseTo(d +/- 0.1)
t1 must beMatching(t2)
Create your own
The easiest way to create a new matcher is to create it from a function returning a tuple with a boolean and one or more messages:
// import the necessary implicit conversions if you are outside of a Specification
// import org.specs2.matcher.MatchersImplicits._
// annotate the return type so that implicit conversions can transform your function into a Matcher object
// here just return a boolean and a failure message
def startWithHello: Matcher[String] = { s: String =>
(s.startsWith("hello"), s+" doesn't start with hello")
}
"hello world" must startWithHello
// with a success message and a failure message
def endWithWorld: Matcher[String] = { s: String =>
(s.endsWith("world"), s+" ends with world", s+" doesn't end with world")
}
"hello world" must endWithWorld
// with a function taking the actual value for the failure message
def startWithHi: Matcher[String] =
((_: String).startsWith("hi"), (_:String)+" doesn't start with hi")
"hi universe" must startWithHi
// with 2 functions for the success and failure messages
def endWithUniverse: Matcher[String] =
((_: String).endsWith("universe"), (s:String) => s+ " ends with universe", (s:String) => s+ " doesn't end with universe")
"hi universe" must endWithUniverse
If you want absolute power over matching, you can define your own matcher extending Matcher:
case class BeMyOwnEmpty() extends Matcher[String] {
def apply[S <: String](s: Expectable[S]) = {
result(s.value.isEmpty,
s.description + " is empty",
s.description + " is not empty",
s)
}
}
"" must BeMyOwnEmpty()
In the code above you have to:
define the apply method (and its somewhat complex signature)
use the protected result method to return: a Boolean condition, a message when the match is ok, a message when the match is not ok, the “expectable” value. Note that if you change the expectable value you need to use the map method on the s expectable (s.map(other)). This way you preserve the ability of the Expectable to throw an Exception if a subsequent match fails
you can use the description method on the Expectable class to return the full description of the expectable including the optional description you setup using the aka method
Now learn how to...
use standard results (failure, success, skipped, todo…) instead of matchers
add descriptions to your expectations to create even better failure messages
use a different equality function or display other failure messages with the Diffable typeclass
use datatables to conveniently group several examples into one
use ScalaCheck to generate and verify data for your examples
use Mockito to mock the interactions with another system
use Forms to display actual and expected values in html tables