Lesson 20 / 25

Testing with MUnit and ScalaTest

Write unit and property-based tests for Scala code.

Testing Scala code

Popular test frameworks include MUnit (simple, fast, with clear diffs, used in this course's examples), ScalaTest (many styles such as AnyFunSuite, AnyFlatSpec and AnyWordSpec, with rich matchers like result shouldBe 42) and weaver or munit-cats-effect for effectful code. Property-based testing with ScalaCheck generates many random inputs and checks that properties always hold, such as "sorting twice equals sorting once" or "encode then decode returns the original", and it shrinks failing inputs to minimal examples. Pure functions and ADTs make tests straightforward: no mocks are needed when logic does not perform I/O. For I/O boundaries, depend on traits and pass test implementations, or use test containers (testcontainers-scala) for databases and message brokers. Test Future and IO code with framework support for asynchronous tests rather than blocking. Run tests with sbt test, or scala test . with Scala CLI, and include coverage with scoverage where it is useful.

Example-based and property-based tests

MUnit suites with a ScalaCheck property.

// src/test/scala/PricingSuite.scala
import munit.ScalaCheckSuite
import org.scalacheck.Prop.forAll
import org.scalacheck.Gen

object Pricing:
  def withGstPaise(paise: Long): Long = paise + (paise * 18 + 50) / 100   // rounds to nearest paisa
  def discountPaise(paise: Long, percent: Int): Long =
    require(percent >= 0 && percent <= 100)
    paise * (100 - percent) / 100

class PricingSuite extends ScalaCheckSuite:
  test("adds 18% GST") {
    assertEquals(Pricing.withGstPaise(10000), 11800L)
  }

  test("rejects invalid discounts") {
    intercept[IllegalArgumentException](Pricing.discountPaise(1000, 120))
  }

  val amounts = Gen.choose(0L, 10_000_000L)
  val percents = Gen.choose(0, 100)

  property("discount never increases the price") {
    forAll(amounts, percents) { (paise, pct) =>
      Pricing.discountPaise(paise, pct) <= paise
    }
  }

  property("GST is never negative and never more than 18% plus rounding") {
    forAll(amounts) { paise =>
      val extra = Pricing.withGstPaise(paise) - paise
      extra >= 0 && extra <= paise * 18 / 100 + 1
    }
  }

// build.sbt: libraryDependencies += "org.scalameta" %% "munit-scalacheck" % "1.1.0" % Test

A thousand monkeys testing your rules

Example tests check the cases you thought of. Property tests send a crowd of random shoppers through your checkout, and when one of them breaks it, ScalaCheck finds the simplest version of that shopper to show you.

Quick check: What does property-based testing with ScalaCheck do?

  • Tests only one hard-coded input
  • Measures performance
  • Formats code
  • Generates many random inputs to check that a property always holds, shrinking failures to minimal examples
Answer

Generates many random inputs to check that a property always holds, shrinking failures to minimal examples — ScalaCheck generates inputs and shrinks counterexamples.