Scala Testing with ScalaTest

ScalaTest is the most widely used testing framework for Scala. It lets you write tests that verify your code behaves correctly, catch regressions when you make changes, and document how your functions are meant to be used. Well-tested code is safer to refactor and easier for new developers to understand.

Adding ScalaTest to Your Project

// build.sbt
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.17" % Test

Your First Test

import org.scalatest.funsuite.AnyFunSuite

class MathUtilsTest extends AnyFunSuite:

  test("add returns the correct sum"):
    val result = 2 + 3
    assert(result == 5)

  test("multiply returns the correct product"):
    assert(4 * 7 == 28)

  test("division by zero throws exception"):
    assertThrows[ArithmeticException]:
      10 / 0

Run tests with sbt test. SBT finds all files ending in Test or Spec and runs them automatically.

Testing Real Functions

// The code under test
def factorial(n: Int): Long =
  if n <= 1 then 1L else n * factorial(n - 1)

def parseAge(s: String): Option[Int] =
  scala.util.Try(s.toInt).filter(n => n >= 0 && n <= 120).toOption

// The tests
import org.scalatest.funsuite.AnyFunSuite

class FunctionTests extends AnyFunSuite:

  test("factorial of 0 is 1"):
    assert(factorial(0) == 1L)

  test("factorial of 5 is 120"):
    assert(factorial(5) == 120L)

  test("factorial of 10 is 3628800"):
    assert(factorial(10) == 3628800L)

  test("parseAge returns Some for valid age"):
    assert(parseAge("25") == Some(25))

  test("parseAge returns None for text"):
    assert(parseAge("abc") == None)

  test("parseAge returns None for negative"):
    assert(parseAge("-5") == None)

  test("parseAge returns None for age over 120"):
    assert(parseAge("150") == None)

Matchers — Readable Assertions

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

class MatcherTests extends AnyFunSuite with Matchers:

  test("string matchers"):
    "Scala" should startWith("Sc")
    "Scala" should endWith("la")
    "Scala" should include("ala")
    "Scala".length should be(5)
    "Scala" should not be empty

  test("number matchers"):
    42 should be > 30
    42 should be < 100
    3.14 should be(3.14 +- 0.01)   // within tolerance

  test("collection matchers"):
    List(1, 2, 3) should have length 3
    List(1, 2, 3) should contain(2)
    List(1, 2, 3) should not contain 5
    List(1, 2, 3) shouldBe sorted
    List(1, 2, 3) should contain allOf(1, 3)

  test("Option matchers"):
    Some(42) shouldBe defined
    None shouldBe empty
    Some("hello") should contain("hello")

FlatSpec Style — Behavior-Driven

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

class CalculatorSpec extends AnyFlatSpec with Matchers:

  "A Calculator" should "add two positive numbers" in:
    val result = 10 + 5
    result should be(15)

  it should "return negative for negative sum" in:
    val result = -3 + (-7)
    result should be(-10)

  it should "handle zero correctly" in:
    0 + 42 should be(42)
    42 + 0 should be(42)

Testing Case Classes

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

case class Product(name: String, price: Double, inStock: Boolean)

def applyDiscount(p: Product, percent: Double): Product =
  p.copy(price = p.price * (1 - percent / 100))

class ProductTests extends AnyFunSuite with Matchers:

  test("applyDiscount reduces price correctly"):
    val product  = Product("Laptop", 80000.0, true)
    val result   = applyDiscount(product, 10.0)
    result.price should be(72000.0 +- 0.01)
    result.name  should be("Laptop")        // name unchanged
    result.inStock should be(true)          // inStock unchanged

  test("applyDiscount does not modify original"):
    val original = Product("Phone", 30000.0, true)
    val discounted = applyDiscount(original, 20.0)
    original.price should be(30000.0)       // original unchanged
    discounted.price should be(24000.0 +- 0.01)

BeforeAndAfter — Setup and Teardown

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.BeforeAndAfter

class DatabaseTests extends AnyFunSuite with BeforeAndAfter:
  var db: scala.collection.mutable.Map[Int, String] = _

  before:
    db = scala.collection.mutable.Map()   // fresh map before each test

  after:
    db.clear()   // clean up after each test

  test("insert and retrieve"):
    db(1) = "Alice"
    assert(db.get(1) == Some("Alice"))

  test("missing key returns None"):
    assert(db.get(99) == None)

  test("delete removes entry"):
    db(5) = "Bob"
    db.remove(5)
    assert(db.get(5) == None)

Running Specific Tests

// Run all tests
$ sbt test

// Run a specific test class
$ sbt "testOnly com.example.MathUtilsTest"

// Run tests whose names match a pattern
$ sbt "testOnly *Calculator*"

// Watch for changes and re-run tests
$ sbt ~test

Test Coverage

// Add to project/plugins.sbt
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9")

// Run with coverage
$ sbt clean coverage test coverageReport
// Opens an HTML report showing which lines are tested

What Makes a Good Test


Property              Description
──────────────────    ─────────────────────────────────────────────────
Fast                  Runs in milliseconds, not seconds
Independent           Each test sets up its own state (no shared state)
Repeatable            Same result every time, no randomness or timing
Readable              Test name says what it tests
Small                 Tests one thing — one assertion per test ideally
Deterministic         No flakiness — never passes randomly and fails sometimes

Testing is not just about finding bugs — it is about confidence. A solid test suite lets you refactor freely, upgrade dependencies, and add features without fear of breaking existing behavior. Make testing a habit from the first line of code you write.

Leave a Comment

Your email address will not be published. Required fields are marked *