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.
