Specs2: Behavior-Driven Specification Testing in Scala
Specs2 takes a different philosophy from ScalaTest: rather than providing multiple testing styles, it treats tests as specifications — executable documents that describe how software should behave. If you want tests that read like documentation and double as acceptance criteria, Specs2 is worth considering.
Installation
libraryDependencies ++= Seq(
"org.specs2" %% "specs2-core" % "5.5.1" % Test
)For property-based testing with ScalaCheck:
"org.specs2" %% "specs2-scalacheck" % "5.5.1" % TestTwo Specification Styles
Specs2 offers two styles, and the choice is architectural, not cosmetic.
Acceptance Specification
Separates the description from the code. The spec reads top-to-bottom like prose:
import org.specs2.Specification
class UserServiceSpec extends Specification {
def is = s2"""
The UserService should
create a user with valid attributes $createUserWithValidAttrs
reject a user with a duplicate email $rejectDuplicateEmail
find a user by ID $findUserById
return None for unknown ID $returnNoneForUnknownId
"""
def createUserWithValidAttrs = {
val service = new UserService
val user = service.create("Alice", "alice@example.com")
user.id must beSome
user.name must beEqualTo("Alice")
}
def rejectDuplicateEmail = {
val service = new UserService
service.create("Alice", "alice@example.com")
service.create("Bob", "alice@example.com") must throwA[DuplicateEmailException]
}
def findUserById = {
val service = new UserService
val user = service.create("Charlie", "charlie@example.com")
service.findById(user.id.get) must beSome.which(_.name == "Charlie")
}
def returnNoneForUnknownId = {
val service = new UserService
service.findById(99999) must beNone
}
}The s2"""...""" string interpolation links prose descriptions to Scala expressions. The table of contents (the string) and the tests (the methods) are kept in sync explicitly.
Mutable Specification (Unit Style)
More familiar to developers coming from JUnit or ScalaTest:
import org.specs2.mutable.Specification
class CalculatorSpec extends Specification {
"Calculator" should {
"add two numbers" in {
Calculator.add(2, 3) must beEqualTo(5)
}
"subtract numbers" in {
Calculator.subtract(10, 4) must beEqualTo(6)
}
"throw for division by zero" in {
Calculator.divide(10, 0) must throwA[ArithmeticException]
}
}
}The mutable style builds the spec by executing the should and in blocks as side effects during construction. It's more concise but less explicit about structure.
Matchers
Specs2 matchers use must rather than should:
// Equality
result must beEqualTo(5)
result must ===(5)
result must be_==(5)
// Negation
result must not(beEqualTo(0))
result must be_!=(0)
// Null / defined
opt must beSome
opt must beNone
opt must beSome.which(_ > 0)
ref must beNull
ref must not(beNull)
// Strings
str must startWith("Hello")
str must endWith("world")
str must contain("testing")
str must beMatching("\\d{4}-\\d{2}-\\d{2}")
// Collections
list must contain(42)
list must containAllOf(Seq(1, 2, 3))
list must haveLength(5)
list must beEmpty
list must not(beEmpty)
// Numeric comparison
n must beGreaterThan(0)
n must beLessThanOrEqualTo(100)
n must beBetween(1, 10)
// Exceptions
expression must throwA[IllegalArgumentException]
expression must throwA[IllegalArgumentException].like {
case e => e.getMessage must contain("negative")
}Combining Expectations
In Specs2, each must expression returns a Result. You combine results with and, or:
def userIsValid = {
val user = service.create("Alice", "alice@example.com")
(user.id must beSome) and
(user.name must beEqualTo("Alice")) and
(user.email must beEqualTo("alice@example.com"))
}Unlike ScalaTest, all expectations in a block are evaluated — you see all failures, not just the first one. This is particularly useful when debugging complex objects.
Effects (Setup and Teardown)
Specs2 uses BeforeEach, AfterEach, BeforeAll, and AfterAll:
import org.specs2.mutable.Specification
import org.specs2.specification.BeforeEach
class DatabaseSpec extends Specification with BeforeEach {
var db: TestDatabase = _
def before = {
db = new TestDatabase
db.migrate()
}
"Database" should {
"persist records" in {
db.insert(Record("test"))
db.count must beEqualTo(1)
}
}
}For the acceptance specification style, use step:
class DatabaseSpec extends Specification {
def is = s2"""
${"Setup" ! step(setupDatabase())}
The database
persists records $persistsRecords
enforces unique constraints $uniqueConstraints
${"Teardown" ! step(teardownDatabase())}
"""
def setupDatabase() = { /* ... */ }
def teardownDatabase() = { /* ... */ }
def persistsRecords = { /* ... */ }
def uniqueConstraints = { /* ... */ }
}Integration with ScalaCheck
Specs2 integrates natively with ScalaCheck for property-based testing:
import org.specs2.ScalaCheck
import org.scalacheck.Prop.forAll
class StringUtilsSpec extends Specification with ScalaCheck {
def is = s2"""
String utilities
encode/decode is a round-trip $roundTrip
length is always non-negative $nonNegativeLength
"""
def roundTrip = forAll { (s: String) =>
StringUtils.decode(StringUtils.encode(s)) must beEqualTo(s)
}
def nonNegativeLength = forAll { (s: String) =>
s.length must beGreaterThanOrEqualTo(0)
}
}Tagging and Filtering
import org.specs2.annotation.Tags
class ServiceSpec extends Specification with Tags {
def is = s2"""
Quick test $quickTest
Slow test $slowTest
Integration $integrationTest
"""
def quickTest = ok
def slowTest = { tag("slow"); ok }
def integrationTest = { tag("integration"); ok }
}Run excluding slow:
sbt "testOnly * -- exclude slow"Parallel Execution
Specs2 runs examples in parallel by default within a specification. Control this with:
class MySpec extends Specification {
sequential // run examples sequentially
def is = s2"""
test 1 $test1
test 2 $test2
"""
}Running Specs2
# All specs
sbt <span class="hljs-built_in">test
<span class="hljs-comment"># Specific spec
sbt <span class="hljs-string">"testOnly com.myapp.UserServiceSpec"
<span class="hljs-comment"># With arguments
sbt <span class="hljs-string">"testOnly * -- include integration"
<span class="hljs-comment"># Watch mode
sbt ~<span class="hljs-built_in">testSpecs2 vs ScalaTest
Choose Specs2 when:
- You want tests to serve as living documentation
- Your team writes acceptance criteria before implementation
- You prefer separating prose descriptions from code
Choose ScalaTest when:
- Your team comes from JUnit/TestNG backgrounds
- You want to choose from multiple testing styles
- You need tight integration with Mockito (ScalaTest integrates more easily)
Browser Testing Complement
Specs2 handles backend specifications. For verifying behavior through a real browser — especially for Play Framework or Akka HTTP applications — HelpMeTest provides continuous browser monitoring. Your Specs2 tests verify the logic; HelpMeTest verifies the experience.
Summary
Specs2 brings BDD thinking to Scala:
- Acceptance specs — separate prose from code, readable as documentation
- Mutable specs — familiar unit-test style with ScalaTest-like structure
- Rich matchers — expressive with
must, combined withand/or - ScalaCheck integration — property testing baked in
- Parallel by default — faster CI with no configuration
If your team writes specifications first and tests second, Specs2's acceptance style maps directly to that workflow.