Scala Testing: ScalaTest and Property-Based Testing

TT

Scala Testing: ScalaTest and Property-Based Testing

Testing in Scala combines familiar unit testing patterns with powerful functional testing tools. ScalaTest provides multiple testing styles, ScalaCheck brings property-based testing, and Mockito handles mocking. This module covers the full testing toolkit for Scala applications.

Setting Up ScalaTest

scala
// build.sbt
libraryDependencies ++= Seq(
  "org.scalatest"  %% "scalatest"       % "3.2.17" % Test,
  "org.mockito"    %% "mockito-scala"   % "1.17.30" % Test,
  "org.scalacheck" %% "scalacheck"      % "1.17.0"  % Test
)

Run tests with sbt test or sbt testOnly *MySpec.

FunSuite: Simple Test Cases

FunSuite is the closest to JUnit — each test is a named function:

scala
import org.scalatest.funsuite.AnyFunSuite

class CalculatorSuite extends AnyFunSuite {

  test("addition works correctly") {
    assert(2 + 2 == 4)
  }

  test("division throws on zero denominator") {
    assertThrows[ArithmeticException] {
      10 / 0
    }
  }

  test("string concatenation") {
    val result = "Hello" + " " + "World"
    assert(result == "Hello World")
    assert(result.length == 11)
  }
}

FlatSpec: BDD-Style Tests

FlatSpec reads like a specification:

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

class StringSpec extends AnyFlatSpec with Matchers {

  "A String" should "support length calculation" in {
    "hello".length should be(5)
  }

  it should "support uppercase conversion" in {
    "hello".toUpperCase should equal("HELLO")
  }

  it should "support contains check" in {
    "hello world" should include("world")
  }

  "An empty string" should "have length zero" in {
    "".length should be(0)
    "" should be(empty)
  }
}

Matchers: Readable Assertions

The Matchers trait gives you expressive assertions:

scala
import org.scalatest.matchers.should.Matchers

// Equality
result should equal(42)
result shouldBe 42
result should not equal 0

// Comparisons
value should be > 0
value should be <= 100

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

// Strings
str should startWith("Hello")
str should endWith("World")
str should include("lo W")
str should fullyMatch regex "[A-Z].*"

// Options
option should be(defined)
option should be(None)
option should contain(42)

// Exceptions
an [IllegalArgumentException] should be thrownBy {
  require(false, "bad input")
}

WordSpec: Full BDD Specification

WordSpec provides the most structured BDD format:

scala
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.should.Matchers

class UserServiceSpec extends AnyWordSpec with Matchers {

  "UserService" when {
    "creating a new user" should {
      "return a User with the given name" in {
        // test body
        val user = User("Alice", 30)
        user.name shouldBe "Alice"
      }

      "assign a unique ID" in {
        val u1 = User("Alice", 30)
        val u2 = User("Bob", 25)
        u1.id should not equal u2.id
      }
    }

    "finding an existing user" should {
      "return Some(user) when found" in {
        // ...
      }

      "return None when not found" in {
        // ...
      }
    }
  }
}

BeforeAndAfter: Setup and Teardown

scala
import org.scalatest.BeforeAndAfter
import org.scalatest.flatspec.AnyFlatSpec

class DatabaseSpec extends AnyFlatSpec with BeforeAndAfter {
  var connection: Connection = _

  before {
    connection = openTestConnection()
  }

  after {
    connection.close()
  }

  "Database" should "insert and retrieve records" in {
    // use connection
  }
}

Mocking with Mockito for Scala

scala
import org.mockito.MockitoSugar
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

trait UserRepository {
  def findById(id: Int): Option[User]
  def save(user: User): Unit
}

class UserServiceSpec extends AnyFlatSpec
    with Matchers
    with MockitoSugar {

  "UserService" should "return user when found" in {
    val mockRepo = mock[UserRepository]
    val service = new UserService(mockRepo)

    when(mockRepo.findById(1)).thenReturn(Some(User("Alice", 30)))

    val result = service.getUser(1)

    result shouldBe Some(User("Alice", 30))
    verify(mockRepo).findById(1)
  }

  it should "return None when user not found" in {
    val mockRepo = mock[UserRepository]
    val service = new UserService(mockRepo)

    when(mockRepo.findById(99)).thenReturn(None)

    service.getUser(99) shouldBe None
  }
}

Property-Based Testing with ScalaCheck

Instead of writing specific examples, ScalaCheck generates random test cases:

scala
import org.scalacheck.{Gen, Properties}
import org.scalacheck.Prop.forAll

object StringProps extends Properties("String") {

  property("reverse twice is identity") = forAll { (s: String) =>
    s.reverse.reverse == s
  }

  property("concatenation length") = forAll { (s1: String, s2: String) =>
    (s1 + s2).length == s1.length + s2.length
  }
}

// Using ScalaCheck within ScalaTest
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalatest.flatspec.AnyFlatSpec

class ListSpec extends AnyFlatSpec with ScalaCheckPropertyChecks {

  "List.reverse" should "be its own inverse" in {
    forAll { (list: List[Int]) =>
      list.reverse.reverse shouldBe list
    }
  }

  it should "preserve elements" in {
    forAll { (list: List[Int]) =>
      list.reverse.sorted shouldBe list.sorted
    }
  }
}

ScalaCheck tries to find a counter-example. If it fails, it "shrinks" the input to the smallest failing case.

Custom Generators

scala
import org.scalacheck.Gen

val positiveInt: Gen[Int] = Gen.posNum[Int]
val nonEmptyString: Gen[String] = Gen.alphaStr.suchThat(_.nonEmpty)

case class Person(name: String, age: Int)

val personGen: Gen[Person] = for {
  name <- Gen.alphaStr.suchThat(_.nonEmpty)
  age  <- Gen.chooseNum(0, 120)
} yield Person(name, age)

forAll(personGen) { person =>
  person.age should be >= 0
  person.name should not be empty
}

Frequently Asked Questions

Q: Which ScalaTest style should I choose — FunSuite, FlatSpec, or WordSpec? FunSuite is best for beginners and teams coming from JUnit — it's familiar and minimal. FlatSpec is popular for library code where tests read as specifications. WordSpec is preferred when writing acceptance-level tests or behavior-driven development (BDD) where the full "given/when/then" structure matters. Pick one style per project and be consistent. If in doubt, start with FlatSpec — it's concise and readable.

Q: What is the main benefit of property-based testing over example-based testing? Example-based tests check specific cases you thought of. Property-based tests check invariants that should hold for all inputs in a domain — and the framework generates hundreds of random inputs to try to break them. Properties often find edge cases you never considered: empty strings, negative numbers, unicode characters, very long inputs. They're especially valuable for pure functions, data transformations, and any code with mathematical invariants.

Q: How do I test Futures in ScalaTest? Use ScalaFutures trait: import org.scalatest.concurrent.ScalaFutures and call whenReady(future) { result => ... } or use futureValue to block and unwrap. Set a PatienceConfig to control how long to wait. For Akka applications, use akka-testkit which provides TestActorRef, TestProbe, and utilities to test actors synchronously.


Part of Scala Mastery Course — Module 21 of 22.