simplespec
(NOTE: This project is no longer actively maintained. For an actively developed replacement, we recommend [ScalaTest] (http://www.scalatest.org). If you have any interest in taking over maintenance and development, please [file an issue] (https://github.com/SimpleFinance/simplespec/issues/new).)
No seriously, keep it simple.
simplespec is a thin Scala wrapper over JUnit, the most commonly-used test framework on the JVM. simplespec was originally written by Coda Hale and was subsequently maintained and developed by Simple until June 2016. The library features extensible Hamcrest matchers, easy mocks, and other niceties.
Requirements
- Scala 2.12.1
- JUnit 4.11
- Mockito 1.9.5
(Scala 2.11.0 & 2.10.2, 2.9.1, and 2.9.2 are supported in simplespec 0.8.4, 0.6.0, and 0.7.0, respectively.)
Getting Started
First, specify simplespec as a dependency.
<dependencies>
<dependency>
<groupId>com.simple</groupId>
<artifactId>simplespec_2.12</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
If you are on Scala 2.11.0, you should use:
<dependencies>
<dependency>
<groupId>com.simple</groupId>
<artifactId>simplespec_2.11</artifactId>
<version>0.8.4</version>
</dependency>
</dependencies>
If you are on Scala 2.10.2, you should use:
<dependencies>
<dependency>
<groupId>com.simple</groupId>
<artifactId>simplespec_2.10.2</artifactId>
<version>0.8.4</version>
</dependency>
</dependencies>
If you are on Scala 2.9.2, you should use:
<dependencies>
<dependency>
<groupId>com.simple</groupId>
<artifactId>simplespec_2.9.2</artifactId>
<version>0.7.0</version>
</dependency>
</dependencies>
And for 2.9.1:
<dependencies>
<dependency>
<groupId>com.simple</groupId>
<artifactId>simplespec_2.9.1</artifactId>
<version>0.6.0</version>
</dependency>
</dependencies>
Second, write a spec:
import com.example.Stack
import org.junit.Test
import com.simple.simplespec.Spec
class StackSpec extends Spec {
class `An empty stack` {
val stack = Stack()
@Test def `has a size of zero` = {
stack.size.must(be(0))
}
@Test def `is empty` = {
stack.isEmpty.must(be(true))
}
class `with an item added to it` {
stack += "woo"
@Test def `might have an item in it` = {
stack.must(be(empty))
}
}
}
}
Execution Model
The execution model for a Spec
is just a logical extension of how JUnit itself works -- a Spec
class contains one or more regular classes, each of which can contain zero or more @Test
-annotated methods or further nested classes.
When JUnit runs the Spec
class, it creates new instances of each class for each test method run, allowing for full test isolation. In the above example, first an instance of StackSpec
would be created, then an instance of StackSpec#`An empty stack`
, then an instance of StackSpec#`An empty stack`#`with an item added to it`
, and finally its `might have an item in it`
method is run as a test.
The tradeoff of this execution model (vs. one which shares state between test invocation) is that tests which create a substantial amount of shared state (e.g., data-intensive tests) spend a lot of time setting up or tearing down state.
Unlike JUnit, simplespec doesn't require your test methods to return void.
The outer Spec
instance has beforeEach
and afterEach
methods which can be overridden to perform setup and teardown tasks for each test contained in the context. simplespec also provides BeforeEach
, AfterEach
, and BeforeAndAfterEach
traits which inner classes can extend to perform more tightly-scoped setup and teardown tasks.
Matchers
simplespec provides a thin layer over Hamcrest matchers to allow for declarative assertions in your tests:
stack.must(be(empty))
simplespec includes the following matchers by default, but you're encouraged to write your own:
x.must(equal(y))
: Assertsx == y
.x.must(be(y))
: A synonym forequal
.x.must(beA(klass))
: Asserts thatx
is assignable as an instance ofklass
.x.must(be(matcher))
: Asserts thatmatcher
applies tox
.x.must(not(be(matcher)))
: Asserts thatmatcher
does not apply tox
.x.must(be(empty))
: Asserts thatx
is aTraversableLike
which is empty.x.must(haveSize(n))
: Asserts thatx
is aTraversableLike
which hasn
elements.x.must(contain(y))
: Asserts thatx
is aSeqLike
which contains the elementy
.x.must(be(notNull))
: Asserts thatx
is notnull
.x.must(be(approximately(y, delta)))
: Asserts thatx
is withindelta
ofy
. Useful for floating-point math.x.must(be(lessThan(2)))
: Asserts thatx
is less than2
.x.must(be(greaterThan(2)))
: Asserts thatx
is greater than2
.x.must(be(lessThanOrEqualTo(2)))
: Asserts thatx
is less than or equal to2
.x.must(be(greaterThanOrEqualTo(2)))
: Asserts thatx
is greater than or equal to2
.x.must(startWith("woo"))
: Asserts that stringx
starts with"woo"
.x.must(endWith("woo"))
: Asserts that stringx
ends with"woo"
.x.must(contain("woo"))
: Asserts that stringx
contains with"woo"
.x.must(`match`(".*oo".r))
: Asserts that stringx
matches the regular expression.*oo
.
Matchers like be
and not
take matchers as their arguments, which means you can write domain-specific matchers for your tests:
class IsSufficientlyCromulentMatcher extends BaseMatcher[Fromulator] {
def describeTo(description: Description) {
description.appendText("a cromulemnt fromulator")
}
def matches(item: AnyRef) = item match {
case fromulator: Fromulator => fromulator.isCromulent
case _ => false
}
}
trait CromulentMatcher {
def cromulent = new IsSufficientlyCromulentMatcher
}
class BlahBlahSpec extends Spec with CromulentMatcher {
class `A Fromulator` {
val fromulator = new Fromulator
def `is cromulent` = {
fromulator.must(be(cromulent)
}
}
}
simplespec also includes two helper methods: evaluating
and eventually
.
evaluating
captures a closure and allows you to make assertions about what happens when it's evaluated:
@Test def `throws an exception` = {
evaluating {
dooHicky.stop()
}.must(throwAn[UnsupportedOperationException])
}
eventually
also captures a closure, but allows you to assert things about what happens when the closure is evaluated which might not be true the first few times:
@Test def `decay to zero` = {
eventually {
thingy.rate
}.must(be(approximately(0.0, 0.001)))
}
See Matchers.scala
for the full run-down.
Mocks
SimpleSpec uses Mockito for mocking stuff. It has its own wrappers around Mockito to make things a bit easier.
class PublisherSpec extends Spec {
class `A publisher` {
val message = mock[Message]
val queue = mock[Queue]
queue.enqueue(any).returns(0, 1, 2, 3)
val publisher = new Publisher(queue)
@Test def `sends a message to the queue` = {
publisher.receive(message)
verify.one(queue).enqueue(message)
}
}
}
Mock Stubbing
By default, when you mock something and call a method on it, the call will return null
or a basic value like 0
or false
for primitives.
If you want to control what the mocked object returns for a given method call, you can use returns
, throws
, or answersWith
:
val foo = mock[FooService]
// .returns() can be used when you just want to return a static value
foo.getNumber("one").returns(1)
foo.getNumber("two").returns(2)
// .throws() will make the call throw the given exception.
// Note: if Mockito complains about a checked exception being invalid, you'll
// need to use .answersWith() to throw the exception instead.
foo.getNumber("dogs").throws(new NumberFormatException)
// .answersWith() will call the function you pass it and use its result
// as the mocked return value.
foo.getNumber("three").answersWith(_ => 3)
foo.getNumber("dogs").answersWith(_ => throw new NumberFormatException)
These stubbing functions are sensitive to order. So this:
foo.get(1).returns("cats")
foo.get(1).returns("dogs")
Will return "dogs"
every time you call foo.get(1)
.
You can also dynamically match arguments in method calls. The simplest way is to use any
to match any argument of a given type:
foo.get(any[Int]).returns(None)
foo.get(1).returns(Some("dogs"))
This example uses the fact that stubs are sensitive to ordering to its advantage.
Note that if you match any of the method's arguments with a dymanic matcher like any
, you'll need to match them all dynamically. For example, this does not work:
foo.get(any[Int], "Hello").returns(...)
You can use equalTo
to get around this:
foo.get(any[Int], equalTo[String]("Hello")).returns(...)
Available dynamic matchers:
any[A](implicit mf: Manifest[A])
: A matcher which will accept any instance.isA[A](implicit mf: Manifest[A])
: A matcher which will accept any instance of the given type.equalTo[A](value: A)
: A matcher which will accept any instance of the given type which is equal to the given value.same[A](value: A)
: A matcher which will accept only the same instance as the given value.isNull[A]
: A matcher which will accept only null values.isNotNull[A]
: A matcher which will accept only non-null values.contains(substring: String)
: A matcher which will accept only strings which contain the given substring.matches(pattern: Regex)
: A matcher which will accept only strings which match the given pattern.endsWith(suffix: String)
: A matcher which will accept only strings which end with the given suffix.startsWith(prefix: String)
: A matcher which will accept only strings which start with the given prefix.
WARNING: Since the matchers are really Java under the hood, they do not understand Scala default arguments. If you are matching against a method with default arguments, you must specify the default arguments as well (Scala calls the method with null
if the default is used.)
Responding to invocations with answersWith
If you have a mock, you can invoke arbitrary behavior when it is called by using answersWith
. This calls a function whenever the mock is used.
This can let you use a fake implementation for the mocked object. It's useful for implementing enough of the functionality to make your code work, or for doing some more advanced checks than normal matchers allow.
myMock.get(any[String]).answersWith { f =>
val stringArg = f.getArguments.toSeq.head.asInstanceOf[String]
println("I was called with " + stringArg)
false // your return value
}
Argument Capture
Simplespec supports Mockito's ArgumentCaptor to capture arguments:
class FooClass {
def concatMethod(x: String, y: Int): String = x + y.toString
}
val arg3 = captor[String]
val arg4 = captor[Int]
val fooMock = mock[FooClass]
fooMock.concatMethod("foo", 1)
verify.one(fooMock).concatMethod(arg3.capture(), arg4.capture())
arg3.getValue().must(be("foo"))
arg4.getValue().must(be(1))
Mock Verification
TODO: Document this.
ScalaCheck
SimpleSpec includes helpers for integrating ScalaCheck properties into your tests, with the hold
and prove
matchers.
class StringPropertySpec extends Spec {
import org.scalacheck.Prop._
class `String operations` {
@Test def startsWith {
forAll((a: String, b: String) => (a+b).startsWith(a)).must(hold)
}
@Test def concatenate {
forAll((a: String, b: String) =>
(a+b).length > a.length && (a+b).length > b.length
).must(hold)
}
@Test def substring {
forAll((a: String, b: String, c: String) =>
(a+b+c).substring(a.length, a.length+b.length) == b
).must(hold)
}
}
}
This is very convenient, since you may mix property and non-property tests freely, and produce test reports & code coverage for your ScalaCheck properties.
License
Copyright (c) 2010-2012 Coda Hale
Copyright (c) 2012-2014 Simple Finance Technology
Published under The MIT License, see LICENSE.md