|
UsingJMock
how to use jMock in your specs
IntroductionjMock2 allows to define mocks and expectations on mocks very easily. This page shows how to use jMock with specs. Please refer to the jMock site for more instructions on jMock itself. You can also have a look at the spec for the jMock integration for more examples. LibrariesIn order to use jMock2 with specs, you need to add the following dependencies to your project: jmock-2.4.0.jar, hamcrest-1.1.jar and cglib-2.1_3.jar, asm-1.5.3.jar, objenesis-1.1.jar if you want to mock classes. Here is a sample maven snippet you can add to your pom.xml file: <dependency>
<groupId>org.jmock</groupId>
<artifactId>jmock</artifactId>
<version>2.4.0</version>
</dependency>
<!-- Those are only needed if you want to mock classes -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>1.0</version>
</dependency>A simple mock example with specs and jMockUsing mocks follows a 4 steps process:
import scala.specs.mock._
object mySpec extends Specification with JMocker with ClassMocker {...}It is especially important that JMocker is mixed-in with your Specification, otherwise the expectations won't be checked 2. create mock objects using the mock method
Important note: the mock declarations should be done either inside your example or in a trait inherited by your specification. Otherwise you will have expectation errors because the expectations are reset before and after each example. One way to do that is to use a doBefore clause: "a statistics component" should {
doBefore {
blogger = mock[Blogger]
stats = new Statistics(blogger)
}
"return the number of posts for today" in {...}
}3. add expectations in your specification example A complete exampleHere is a complete example of the use of mocks to specify the interactions between a Button and a Light object: trait ButtonAndLightMock extends ButtonAndLight with JMocker with ClassMocker {
var mock: Light = _
var button: Button = _
def init = {
mock = mock[Light]
button = Button(mock)
}
}
trait ButtonAndLight {
case class Button(light: Light) {
var lightOn = false
def push = {
if (lightOn) light.off else light.on
lightOn = !lightOn
}
}
case class Light {
var state: LightState = Off
def on = state = On
def off = state = Off
def isOn = state == On
}
abstract sealed class LightState(s: String)
object On extends LightState("on")
object Off extends LightState("off")
}
object mockExample extends Specification with ButtonAndLightMock {
"A button and light mock example" should {
doBefore { init }
"not fail if the mock receives the expected messages" in {
expect {
one(mock).on
one(mock).off
}
button.push
button.push // if the button is pressed twice, then the light will go on and off
}
}
}Use JMock without a SpecificationYou may want to use JMock without necessarily creating a specification (if you use ScalaTest only for example). In that case, you can just import the JMocker object and use its functionalities directly: package org.specs.mock
import org.specs.mock.JMocker._
import org.specs.mock.JMocker.{expect => expecting}
import org.scalatest.Suite
/**
* This sample class shows how to use ScalaTest with JMocker and how to avoid naming conflicts with the <code>expect</code> method
*/
class jMockerWithScalaTestSuite extends Suite {
def testMockExpectations {
val list: java.util.List[Object] = mock[java.util.List[Object]]
expecting { one(list).size }
list.size
checkContext
}
}Methods expectationsMethods countersAll the standard jMock method expectations are available with specs. However, a little bit of syntactic sugar has been added: expect {
exactly(2).of(mock).on // is equivalent to
2.of(mock).on
atLeast(2).of(mock).on // is equivalent to
2.atLeastOf(mock).on
(2 to 4).of(mock).on // between 2 and 4 calls to list.size
}Allowing or ignoring methodsSome shortcuts are available to allow or ignore some method calls: expect {
allowingMatch("on") // allow any method matching "on"
allowingMatch(mock, "on") // allow any method matching "on" on the object "mock"
ignoringMatch("on") // ignore any method matching "on"
ignoringMatch(mock, "on") // ignore any method matching "on" on the object "mock"
}Returned valuesScala allows a chain method call expectations with the specification of the returned values with will, willReturn and willReturnIterable: expect {
one(List("hey")).take(anyInt) will returnValue(equal(List("hey"))) // `will` accepts a jMock action, like returnValue
one(List("hey")).take(anyInt) willReturn List("hey") // `willReturn` specifies a returned value
one(List("hey")).take(anyInt) willReturnIterable("a", "b") // `willReturnIterable` returns an Iterable with specified values
}Returned values on consecutive callsspecs offers the willReturnEach method to specify that consecutive calls to a given method will return different values: // returns "a" the first time the "get" method is called, "b" the second time and "c" the third time
1.atLeastOf(list).get(anyInt) willReturnEach ("a", "b", "c") Nested returned values and mocksThere is a frequent situation when interacting with object graphs. You need to mock an object, like a Connection, which is supposed to give you access to a service, that you also want to mock and so on. For example, testing some code accessing the Eclipse platform can be very difficult for that reason. Using specs you can use blocks to specify nested expectations: // A workspace gives access to a project and a project to a module
case class Module(name: String)
case class Project(module: Module, name: String)
case class Workspace(project: Project)
val workspace = mock[Workspace]
expect {
one(workspace).project.willReturn[Project] {p: Project =>
// nested expectations on project
one(p).name willReturn "hi"
one(p).module.willReturn[Module] {m: Module =>
// nested expectation on module
one(m).name willReturn "module"}
}
}
or // a workspace is a list of projects
case class Project(name: String)
case class Workspace(projects: List[Project])
val workspace = mock[Workspace]
expect {
// the workspace will return project mocks with different expectations
one(workspace).projects willReturnIterable[Project](
{p: Project => one(p).name willReturn "p1" },
{p: Project => one(p).name willReturn "p2" })
}
Capturing parameters for returned valuesSometimes, you may want a mocked method to return the value of one of the passed parameter. You can do that using CapturingParameters: val s = capturingParam[String]
classOf[ClassToMock].expects(one(_).method(s.capture) willReturn s) in {
_.method("a") must_== "a" // the String parameter of the method will be captured and returned
}Note that if the method has several parameters you need to indicate the index of the captured parameter during the capture: val s = capturingParam[String]
classOf[ClassToMock].expects(one(_).method2(anyString, s.capture(1)) willReturn s) in {
_.method2("a", "b") must_== "b" // "b" is returned as the second parameter (this is a 0 based index hence the 'capture(1)')
}It is also possible to apply matchers on capturing parameters, like this: val s = capturingParam[String]
classOf[ClassToMock].expects {
one(_).method(s.must(beMatching("h.*")).capture willReturn s
} in {
_.method("hello") must_== "hello"
}And if necessary, you can transform the captured value using map to return a value of a different type: val s = capturingParam[String]
classOf[ClassToMock].expects(one(_).method3(s.capture) willReturn s.map(_.size)) in {
_.method3("a") must_== 1
}Thrown exceptionsYou can declare that a method call will throw an exception with willThrow: expect {
one(list).get(will(beEqual(0))) willThrow new java.lang.Exception("ouch")
}Method parametersShortcuts are provided for the most common types of parameters: // accepts any value, but can't be mixed with other matchers in a method call anyInt, anyLong, anyShort, anyByte, anyDouble, anyFloat, anyChar, anyString any(<anything>) // checks the class of the parameter a[Type] // returns always true, can be mixed with other matchers in a method call aNull[Type] aNonNull[Type] equal(value) same(value) Known issue: as of now, jMock parameter matching doesn't seem to work with lazy parameters Known issue: working with repeated parameters is tricky. However the following works: case class Param(name: String)
trait ToMock { def method(p: Param*) = () }
val mocked = mock[ToMock]
expect { 1.of(mocked).method(any[Param]) }
mocked.method(Param("hello"))Matchers adaptationA lot of jMock methods are expecting Hamcrest matchers: expect {
one(list).get(`with`(same(0))) // same(0) == new IsSame(0). Note the `backticks` on "with" which is a Scala reserved keyword
}Thanks to an implicit conversion from specs matchers to Hamcrest matchers, you can use specs matchers instead, using the will method: expect {
one(list).get(will(beEqual(0))) // beEqual(0) is a specs matchers
}Note: the be== matcher can't be used here as it is expecting a value of type Any. This is why you should use beEqualT(a: T). Sequence expectationsYou can constrain calls to occur in sequence, with the then method: expect {
one(list).size then
one(list).get(anyInt) then
one(list).isEmpty
}State expectationsYou can model expectations on some abstracts states, using the when and set methods val readiness = state("readiness")
readiness.startsAs("not ready")
expect {
one(list).size set readiness.is("ready")
allowing(list).get(anyInt) when readiness.is("ready")
}Counting expectationsYou can say that some expectations should be counted as such in the statistics: expect {
one(list).get(anyInt) will(returnValue("hey")) isExpectation
}
list.get(0) must_== "hey"This will report 2 expectations (one for the mock expectation, the other one for the equality). One linersIf you want to express only one expectation in your example, you can use this short form: // example 1
classOf[Channel].expects(one(_).isOpen willReturn false) in { _.isOpen must beFalse}
// example 2
classOf[OutputStream].expectsOne(_.flush) in { _.flush }
// example 3
classOf[OutputStream].neverExpects(_.close) in { stream: OutputStream) =>
// use the stream mock object
}One-liners are also very effective at creating mocks for objects with just one expectation or which will be ignored: val sender = classOf[MailSender].expectsOne(_.send).mock // returns a MailSender mock with one expectation val output = classOf[OutputStream].isIgnored.mock // returns a mock which ignores everything publisher(sender, output).publish |
Sign in to add a comment
Link for asm-1.5.3 is http://repo1.maven.org/maven2/asm/asm/1.5.3, not http://repo1.maven.org/maven2/asm/asm/1.3.5. It doesn't work with 1.3.5.
I corrected the page. Thanks!