I've done some programming in Scala, and I know that, e.g.,
xs map f
is the same thing as
xs.map(f)
but I have no idea how to generalize this syntax to something like ScalaTest's syntax, e.g.,
it should "throw NoSuchElementException if an empty stack is popped" in {
val emptyStack = new Stack[String]
evaluating { emptyStack.pop() } should produce [NoSuchElementException]
}
I'm mainly wondering about the things that look like multi-word constructs, namely should produce
. It's neat.
ScalaTest is one of the most popular, complete and easy-to-use testing frameworks in the Scala ecosystem. Where ScalaTest differs from other testing tools is its ability to support a number of different testing styles such as XUnit and BDD out of the box.
Matchers , but uses the verb must instead of should . The two traits differ only in the English semantics of the verb: should is informal, making the code feel like conversation between the writer and the reader; must is more formal, making the code feel more like a written specification.
That kind of syntax is method calls in operator notation, but carried forward more than just three tokens. As you already mentioned:
xs map f
means:
xs.map(f)
But you could go further and say:
xs map f map g
which means:
xs.map(f).map(g)
In ScalaTest matchers, for example, you could say:
result should not be null
That gets desugared by the compiler to:
result.should(not).be(null)
This:
it should "throw an exception" in { ... }
gets desugared into:
it.should("throw an exception").in { ... }
The curly braces at the end is really just a way to pass the code in between the curly braces (the test code) into the in method, wrapped as a no-arg function. So all of these are the same idea. Operator notation used twice in a row.
The last one you asked about is a tad different:
evaluating { ... } should produce [IllegalArgumentException]
This gets transformed into:
evaluating { ... }
is, well, evaluated first, because the curly braces give it precedence. So that is a method call, you are calling a method named "evaluating", passing in the code in between the curly braces as a no-arg function. That returns an object, on which should
is invoked. So should
is a method on the object returned by invoking evaluating
. What should
actually takes is the result of invoking produce
. Here produce
is actually a method, which has a type parameter such as [IllegalArgumentException]
. It must be done this way so the Scala compiler can "poor-man's-reify" that type parameter. It passes an implicit "Manifest" parameter into produce
that can provide the java.lang.Class
instance for IllegalArgumentException
. When that should method is invoked, therefore, it has a function containing the code passed to evaluating
, and a way to find the java.lang.Class
of the exception type put in the square brackets. So it executes the block of code wrapped in a try
, catches the exception, compares it with what's expected. If no exception is thrown, or the wrong one, the should
method throws a TestFailedException
. Otherwise the should
method just returns silently.
So, the answer is that line gets desugared into:
(evaluating { ... }).should(produce[IllegalArgumentException] (compilerSuppliedManifest))
And the moral of the story is that high level code like this makes it easier to see the programmer's intent, but often harder to understand how the code actually works. Most of the time in practice all you care about is the intent, but now and then you need to know how something works. In such cases in Scala you can pass -Xprint:typer
as a command line arg to the Scala compiler and it will print out a version of your file after all the desugaring has happened. So you can see what's what when you need to.
It's reasonably easy because it's normal code - it should
is equivalent to it.should
hence there must be a value (or method) called it
in scope. And there is!
This variable is of type ItWord
which exposes a method called should
which takes an object of type BehaveWord
. These matchers are mixed in via implicit conversions in the ShouldMatchers
trait
.
ScalaTest is actually extremely well documented with tons of examples and descriptions of how things work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With