ScalaTest Guide: The Most Popular Scala Testing Framework

ScalaTest Guide: The Most Popular Scala Testing Framework

ScalaTest is the most widely used testing framework in the Scala ecosystem. It supports multiple testing styles, integrates with popular mocking libraries, and works with both Scala and Java code. Whether you prefer unit tests that look like specs or simple function-based tests, ScalaTest has a style that fits.

Installation

Add ScalaTest to your build.sbt:

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.18" % Test

Run tests with:

sbt test
sbt testOnly com.myapp.UserServiceSpec

Choosing a Test Style

ScalaTest offers several test styles through different traits. The most common are:

Simple, readable syntax that separates the subject from the behavior:

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class CalculatorSpec extends AnyFlatSpec with Matchers {

  "Calculator" should "add two numbers" in {
    val calc = new Calculator
    calc.add(2, 3) should be(5)
  }

  it should "subtract numbers" in {
    val calc = new Calculator
    calc.subtract(10, 4) should be(6)
  }

  it should "throw an exception for division by zero" in {
    val calc = new Calculator
    an[ArithmeticException] should be thrownBy {
      calc.divide(10, 0)
    }
  }
}

AnyWordSpec (BDD Style)

Nested structure with describe-like blocks:

import org.scalatest.wordspec.AnyWordSpec

class UserServiceSpec extends AnyWordSpec with Matchers {

  "UserService" when {
    "creating a user" should {
      "return the created user with an ID" in {
        val service = new UserService
        val user = service.create("Alice", "alice@example.com")
        user.id should not be empty
        user.name should be("Alice")
      }

      "reject duplicate emails" in {
        val service = new UserService
        service.create("Alice", "alice@example.com")
        an[DuplicateEmailException] should be thrownBy {
          service.create("Bob", "alice@example.com")
        }
      }
    }
  }
}

AnyFunSuite (JUnit-like)

Familiar to developers coming from Java:

import org.scalatest.funsuite.AnyFunSuite

class StringUtilsSuite extends AnyFunSuite {

  test("capitalize first letter") {
    assert(StringUtils.capitalize("hello") === "Hello")
  }

  test("return empty string unchanged") {
    assert(StringUtils.capitalize("") === "")
  }
}

Matchers

ScalaTest's matchers make assertions readable. Mix in Matchers to access them:

// Equality
result should be(5)
result should equal(5)
result shouldEqual 5
result should ===(5)

// Not
result should not be(0)
result should not equal("unexpected")

// Comparison
result should be > 0
result should be >= 5
result should be < 100

// Strings
str should startWith("Hello")
str should endWith("world")
str should include("elixir")
str should fullyMatch regex("\\d{4}-\\d{2}-\\d{2}")

// Collections
list should contain(42)
list should contain allOf(1, 2, 3)
list should have length 5
list shouldBe empty
list should not be empty

// Options
opt should be(defined)
opt should contain("value")

Testing Exceptions

Three ways to test for exceptions:

// Assert exception is thrown
an[IllegalArgumentException] should be thrownBy {
  riskyOperation(-1)
}

// Inspect the exception
val ex = the[IllegalArgumentException] thrownBy {
  riskyOperation(-1)
}
ex.getMessage should include("negative")

// Functional style
intercept[ArrayIndexOutOfBoundsException] {
  Array(1, 2, 3)(5)
}

Fixtures

Shared state between tests is managed with fixtures. The cleanest approach for mutable state is BeforeAndAfter:

import org.scalatest.BeforeAndAfter

class DatabaseSpec extends AnyFlatSpec with Matchers with BeforeAndAfter {
  var db: TestDatabase = _

  before {
    db = new TestDatabase
    db.migrate()
  }

  after {
    db.cleanup()
  }

  "Database" should "persist users" in {
    db.insert(User("Alice"))
    db.find("Alice") should be(defined)
  }
}

For functional, composable fixtures prefer fixture.Builder:

def withDatabase(test: TestDatabase => Any): Unit = {
  val db = new TestDatabase
  db.migrate()
  try test(db)
  finally db.cleanup()
}

"Database" should "persist users" in withDatabase { db =>
  db.insert(User("Alice"))
  db.find("Alice") should be(defined)
}

Tagging Tests

Tags let you run subsets of tests:

import org.scalatest.Tag

object Slow extends Tag("com.myapp.Slow")
object Integration extends Tag("com.myapp.Integration")

class SomeSpec extends AnyFlatSpec {

  "fast operation" should "complete quickly" in {
    // runs always
  }

  "slow operation" should "process large dataset" taggedAs Slow in {
    // excluded with -l com.myapp.Slow
  }
}

Run excluding slow tests:

sbt "testOnly * -- -l com.myapp.Slow"

Async Testing

For testing Future-based code, use AsyncFlatSpec:

import org.scalatest.flatspec.AsyncFlatSpec
import scala.concurrent.Future

class UserServiceAsyncSpec extends AsyncFlatSpec with Matchers {

  "UserService" should "fetch user asynchronously" in {
    val service = new UserService

    service.findById(42).map { user =>
      user.name should be("Alice")
      user.id should be(42)
    }
  }

  it should "return failed future for unknown user" in {
    val service = new UserService

    recoverToSucceededIf[UserNotFoundException] {
      service.findById(9999)
    }
  }
}

Running Specific Tests

# Run all tests
sbt <span class="hljs-built_in">test

<span class="hljs-comment"># Run a specific spec
sbt <span class="hljs-string">"testOnly com.myapp.UserServiceSpec"

<span class="hljs-comment"># Run specs matching a pattern
sbt <span class="hljs-string">"testOnly *Spec"

<span class="hljs-comment"># Run only tests with a specific tag
sbt <span class="hljs-string">"testOnly * -- -n com.myapp.Integration"

<span class="hljs-comment"># Run continuously (watch mode)
sbt ~<span class="hljs-built_in">test

Parallel Execution

Configure parallel test execution in build.sbt:

// Run test suites in parallel
Test / parallelExecution := true

// But run tests within a suite sequentially (default)

Or per-suite with ParallelTestExecution:

class MyParallelSpec extends AnyFlatSpec
    with Matchers
    with ParallelTestExecution {

  // Tests in this suite run in parallel
  "service" should "handle concurrent requests" in { ... }
}

Testing Full User Flows

ScalaTest handles backend logic well. For Scala web applications (Play Framework, http4s), verifying complete user flows in a browser requires a different tool. HelpMeTest runs Robot Framework tests against your live application, monitoring continuously and alerting on failures — useful for catching integration issues between your Scala services and their front-ends.

Summary

ScalaTest gives you flexibility to match your team's testing style:

  • AnyFlatSpec for readable, behavior-focused specs
  • AnyWordSpec for BDD-style nested descriptions
  • AnyFunSuite for JUnit-familiar syntax
  • Rich matchers for expressive assertions
  • AsyncFlatSpec for Future-based testing
  • BeforeAndAfter / fixture builders for shared setup/teardown

Start with AnyFlatSpec with Matchers — it's the most widely adopted combination, and your tests will look immediately familiar to other Scala developers.

Read more