|
MatchersGuide
How to add expectations to your examples
Matchers presentationThere are different kind of matchers which are used to assert that some properties must be verified. Generally they are used like this: "This example presents a matcher" in {
// verifyThisProperty is a Matcher
myObject must verifyThisProperty(parameter)
}Be/Have matchersMost of the matchers presented below are used with myObject must matcher. However some matchers are starting with the words 'be' or 'have'. Those matchers also have alternate forms allowing them to be used with 'be' or 'have' as separated words:
// this is equivalent
"hello" must beMatching("h.*")
"hello" must be matching("h.*")
// this is equivalent
"hello" must not(beMatching("z.*"))
"hello" must not be matching("z.*")
// this is equivalent
List("hello") must not(haveSize(2))
List("hello") must not have size(2)
// articles are also allowed
Map("hello" -> "world") must have the key("hello")
// be and have matchers can be combined with logical operators too
"hello" must be matching("h.*") and not be matching("z.*")Create your own MatcherCreating a new Matcher is easy. You extend the Matcher class, using a case class and implement the apply method: import org.specs.matcher.Matcher
"A matcher" can {
"be created as a case class" in {
case class matchHello(a: String) extends Matcher[String]() {
def apply(v: => String) = (v == a, "okMessage", "koMessage")
}
"hello" must matchHello("hello")
}
}You must return a tuple containing:
A matcher can also be created through as a val: import org.specs.matcher.Matcher
"A matcher" can {
"be created as a val" in {
val beEven = new Matcher[Int] {
def apply(number: => Int) = {
val b = number
(b % 2 == 0, b + " is even", b + " is odd")
}
}
2 must beEven
}
}Or through a method: import org.specs.matcher.Matcher
"A matcher" can {
"be created as a method" in {
def divide(a: Int) = new Matcher[Int] {
def apply(number: => Int) = {
val b = number
(a % b == 0, b + " divides " + a, b + " doesn't divide " + a)
}
}
10 must divide(100)
3 must not(divide(100))
}
}Implementation note: in the above examples, the lazy parameter to the apply method is passed to a local val. This avoids unnecessary evaluations when the parameter is also reused to create the result messages. Create the negation of a matcherSimply use the not method on a matcher: 2 must beEven // beEven is a user-made matcher checking if a number is even val beOdd = beEven.not 3 must beOdd In that case, the ok message of the first matcher is used as a ko message for the second matcher. Create a Be/Have matcherWhen you create a matcher like beEven, you may want it to be also usable with be as a separated words. In order to do so, you need to add an implicit conversion from org.specs.specification.Result[T] (which is the result value of the expression a must be) and add the expected matcher as a method: implicit def toOddEvenMatcherResult(result: Result[T]) = new OddEvenMatcherResult(result)
class OddEvenMatcherResult(result: Result[T]) {
def even = result.matchWithMatcher(beEven)
def odd = result.matchWithMatcher(beEven.not)
}Combine matchersYou can combine matchers with logical operators: and, or, xor, verifyAll, verifyAny "ab" must (beMatching("a") and beMatching("b"))
"ab" must (beMatching("a") or beMatching("c"))
"ab" must (beMatching("a") xor beMatching("c"))
"ab" must verifyAll(beMatching("a"), beMatching("b"))
"ab" must verifyAny(beMatching("a"), beMatching("b"))verifyAll must be ok for all matchers and verifyAny must be ok for at least one matcher. Limit the applicability of a matcherIn some cases, you may want to specify that a matcher is only applicable depending on some conditions: "abc" must beMatching(s).when(s == ".") "abc" must beMatching(s).unless(s.isEmpty) Matchers applicable to any object
Note that the eq operator can be very tricky, so be careful to use it on purpose. See issue 40 for more details.
variable arguments can also be used with beOneOf:
List(1, 2) must beLike { case x::y::Nil => true }
a must beNull.when(b == null) b must beNull.when(a == null)
You can also introduce a failure with the fail function:
If you want to verify that an exception is thrown
Or we can check the exception type and message:
Or we can use pattern matching for more specific checks:
Matchers applicable to Strings
Smart differencesThe default failure message for string comparisons is using "smart differences", that is, if the two strings to compare are short (less than 30 characters) the strings are shown as they are. If the strings are longer, the "detailed diffs" message described below is enabled with 30 as a triggering size for shortening and 20 as the shortening size. Detailed differencesYou can get more detailed failure messages for String comparisons with the detailedDiffs method: object SpecificationWithDetailedFailures extends Specification {
detailedDiffs() // enable detailed differences
"abc" must_== "ab" // 'ab[c]' is not equal to 'ab'
"abcd" must_== "abd" // 'ab[c]d' is not equal to 'abd'
"acd" must_== "abd" // 'a[c]d' is not equal to 'abd'
// 'the kitt[en] is pret[ty]' is not equal to 'the [s]kitt[y] is pret[en]'
"the kitten is pretty" must_== "the skitty is preten"
}Note that the pair of separators used to highlight the differences can be changed with: detailedDiffs("()")
// or detailedDiffs("<<>>") to separate with << and >>You can also specify that the detailed diffs should only be triggered when the strings are more than 10 characters long: detailedDiffs("()", 10)And finally, it is possible to declare that the strings should be shortened between the differences in order to be able to spot them more rapidly: detailedDiffs("()", 10, 5) // 5 is the shorten size
'...aaaaa[bb]aa...aa[cc]aaaaa...' is not equal to '...aaaaa[xx]aa...aa[yy]aaaaa...'
"aaaaaaabbaaaaaaaaaaaccaaaaaaaa" must_== "aaaaaaaxxaaaaaaaaaaayyaaaaaaaa"
Matchers applicable to Iterables
List(1, List(2, 3, List(4)), 5) must haveTheSameElementsAs(List(5, List(List(4), 2, 3), 1)) Matchers applicable to Maps
Matchers applicable to numerical values
Matchers applicable to Options
Some(x) must beSome[String].which(_.startWith("abc"))Matchers applicable to ScalaCheck propertiesLet's say you want to implement a function which returns all the prefixes of a given list: prefixes(List(1, 2, 3)) // => List(List(1), List(1, 2), List(1, 2, 3)) You can use ScalaCheck generators to generate lists of random size: // list is an arbitrary list (with at least one element)
// prefix is a random prefix of list
// testData contains all the prefixes of list and a random prefix
val testData = for (list <- listOf1(elements(1, 2, 3, 4));
n <- choose(1, list.size-1);
val prefix = list.take(n))
yield (prefixes(list), prefix)Then, mixing the org.specs.ScalaCheck trait to your specification, you can check that the prefix property passes all generated data: // the generated data must pass the following property
testData must pass { t: (List[List[Int]], Seq[Int]) => val (prefixes, prefix) = t
prefixes must contain(prefix)
}
the above matcher. The failure message will be however less precise
function must pass(generated_data) instead of generated_data must pass(function)
import org.scalacheck.Prop // this property will alway be true val prop = Prop.forAll((a:Int) => true) prop must pass Note: the property function is deprecated from ScalaCheck 1.5. You should use Prop.forAll instead. ScalaCheck parameters
data must pass {
...
}(set(minSize -> 10, maxSize -> 20, maxDiscarded -> 30, minTestsOk -> 5))where:
You can also set the properties and display ScalaCheck messages with display instead of set: data must pass {
...
}(display(minSize -> 10, maxSize -> 20, maxDiscarded -> 30, minTestsOk -> 5))One linersIt is possible to specify properties directly with the verifies operator: object StringSpecification extends Specification("String") with Scalacheck {
"startsWith" verifies { (a: String, b: String) => (a + b).startsWith(a) }
"endsWith" verifies { (a: String, b: String) => (a + b).endsWith(b) }
}This will create examples which names are "startsWith" and "endsWith". Note that you can still use ScalaCheck parameters to control the test generation: "startsWith" verifies ((a: String, b: String) => (a + b).startsWith(a)).set(minTestsOk->250) Expectations numberBy default, the statement property must pass will count each evaluation as one expectation. You can deactivate this behavior by declaring dontExpectProperties at the beginning of your specification and get finer control of what's counted as an expectation with isExpectation: dontExpectProperties() Prop.forAll((b: Boolean) => (b == true).isExpectation) // or Prop.forAll((b: Boolean) => isExpectation(b == true)) Of course, you can globally re-enable expectations counting with expectProperties for the rest of your specification: object spec extends Specification with Scalacheck {
// no expectations count when evaluating properties
dontExpectProperties()
Prop.forAll((b: Boolean) => b == true)
// one expectation will be counted on each property evaluation
expectProperties()
Prop.forAll((b: Boolean) => b == true)
}Matchers applicable to XML
<a><b/></a> must \("b") // or <a><b/></a> must \(<b/>)
<a><b><c><d></d></c></b></a> must \\("c").\("d")
<a><b name="value" name2="value"></b></a> must \("b", ("name2", "name")) // with the Sugar object implicitly transforming tuples to Lists
// just checks the presence of "name" -> "value"
<a><b name="value" name2="value2"></b></a> must \("b", "name"->"value")val actual = <a><b name="value" name2="value2"></b></a>
// using 'have' + a pair of attribute/value (it is a varargs parameter)
actual must have \("b", "name"->"value")
<a><b name="value"></b></a> must \(<b name="value"/>)
<a> <b name="value"> </b> </a> must ==/(<a><b name="value"></b></a>) This matcher doesn't check the nodes order, if you want to check the node order, add ordered to the matcher: <a> <b/> <c/> </a> must ==/(<a><b/></c></a>).ordered Matchers applicable to Paths and FilesMost of the time, you should try to minimize the interactions with the File System. However, sometimes this is precisely what you want to specify! The following matchers will help you specify paths and files properties. For paths (as strings):
For files:
Note: for the above matchers, when paths are compared, separators are always ignored. Matchers composition for object graphsYou may want to combine several existing matchers to be able to match an entire object graph. For example, given the following class definitions: trait ObjectGraph {
import scala.collection.mutable
case class Foo(val name:String) {
var singlebar: Bar = _
val bars = mutable.Set[Bar]()
}
case class Bar(val id: Long)
}You will define the following matchers for the classes Foo and Bar: import org.specs.matcher._
trait ObjectGraphMatchers extends ObjectGraph with Matchers {
case class matchFoo(foo: Foo) extends Matcher[Foo] {
def apply(other: => Foo) = {
((beEqualTo(_:String)) ^^^ ((_:Foo).name) and
(matchOptionalBar(_)) ^^^ ((_:Foo).singlebar) and
(matchBar(_)).toSet ^^^ ((_:Foo).bars))(foo)(other)
}
}
case class matchOptionalBar(bar: Bar) extends Matcher[Bar] {
def apply(other: => Bar) = {
((beAsNullAs(_:Bar)) or
((matchBar(_))))(bar)(other)
}
}
case class matchBar(bar: Bar) extends Matcher[Bar] {
def apply(other: => Bar) = {
((beEqualTo(_:Long)) ^^^ ((_: Bar).id))(bar)(other)
}
}
}In the code above:
Precise failuresMost of the failure messages display an expected value and an actual value. But an actual value on what precisely? // will fail with "List(ticket1, ticket2) doesn't have size 3" for example machine.tickets must haveSize(3) // machine is a user-defined object If you wish to get a more precise failure message on what's being tested you can set an alias with the "aka" method ("also known as"): // will fail with "the created tickets 'List(ticket1, ticket2)' doesn't have size 3" machine.tickets aka "the created tickets" must haveSize(3) xUnit assertionsspecs provides xUnit-like assertions to allow a smooth transition path from Test-Driven Development to Behavior-Driven Development: import org.specs.matcher._
object jUnitTest extends Specification with xUnit {
"provide the 'assertTrue' jUnit assertion" {
assertTrue(1 == 1 + 1)
}
}
|
Sign in to add a comment
That closeTo assertion is wrong perhaps.
Thanks, I corrected it.
Hmm, I guess the rest of us can't edit the wiki? Anyway, there should be an entry in the table of contents for the "Matchers applicable to Strings" section. Cheers.
Good catch Aaron! Too bad I'm not notified of comments, I just saw yours today.