Specs2: Behavior-Driven Specification Testing in Scala

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" % Test

Two 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">test

Specs2 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 with and/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.

Read more