Skip to Content
Course content

36: Testing with ScalaTest

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to almost every developer transitioning into Scala: you write a piece of logic that involves asynchronous calls—usually Futures—and then you write a test for it. You run the test, it turns green, and you feel great. Then, you intentionally break the production code to see if the test catches it, and the test still turns green. It's a frustrating moment, but it's a rite of passage.

case class User(id: Int, name: String)

class UserRepository {
  def findUserById(id: Int): Future[Option[User]] = {
    // Simulating a DB call that might fail or return nothing
    Future {
      if (id == 1) Some(User(1, "Alice")) else None
    }
  }
}

class UserRepositorySpec extends AnyFlatSpec with Matchers {
  val repo = new UserRepository()

  "UserRepository" should "find a user by id" in {
    val result = repo.findUserById(1)
    // This looks right, but it's actually a bug!
    result should be (a [Future[_]]) 
  }
}

The Silent Success of Unresolved Futures

Look closely at that test. You're asserting that result is a Future. Technically, that's true! The method returns a Future immediately, and the test finishes before the code inside that Future ever actually executes. You aren't testing the value inside the box; you're just testing that the box exists. If you changed the repository to always return None, this test would still pass because it's not waiting for the asynchronous operation to complete.

Waiting for the Truth with ScalaFutures

To fix this, we need to tell ScalaTest to actually wait for the Future to resolve before evaluating the assertion. The most common way to do this without switching your entire class hierarchy is by mixing in ScalaFutures. This gives you the whenReady block, which suspends the test until the Future completes (or times out).

import org.scalatest.concurrent.ScalaFutures
import org.scalatest.time.{PatienceConfig, Span}

class UserRepositorySpec extends AnyFlatSpec with Matchers with ScalaFutures {
  // I usually set a custom timeout because the default can be a bit aggressive 
  // depending on your CI environment.
  implicit val patience: PatienceConfig = PatienceConfig(timeout = Span(5, seconds))

  val repo = new UserRepository()

  "UserRepository" should "return Alice when searching for ID 1" in {
    val result = repo.findUserById(1)
    
    whenReady(result) { userOpt =>
      userOpt should be (Some(User(1, "Alice")))
    }
  }

  it should "return None for a non-existent user" in {
    val result = repo.findUserById(99)
    
    whenReady(result) { userOpt =>
      userOpt should be (None)
    }
  }
}

By using whenReady, we've forced the test to synchronize. Now, if the repository returns the wrong user or fails, the test will actually fail. I prefer this approach over manually calling Await.result because it integrates better with ScalaTest's reporting and allows for cleaner timeout configurations.

Writing Expressive Tests with Matchers

One of the best parts of ScalaTest is the Matchers trait. It allows us to move away from the rigid assert(x == y) style and toward something that reads like a sentence. This isn't just about aesthetics; when a test fails, Matchers provides much more descriptive error messages (e.g., "Expected Some(User(1, 'Alice')) but got None") compared to a simple "false was not true".

Depending on what you're testing, you have a few powerful options:

  • should be (value): The bread and butter for equality.
  • should contain (element): Perfect for checking if a List or Set has a specific item.
  • should not be (value): For verifying the absence of a state.
  • intercept[Exception] { ... }: This is how you test that your code correctly throws an error when given bad input.

I'll leave you with a tip: stick to one style. ScalaTest offers FunSpec, WordSpec, FlatSpec, and others. They all do basically the same thing, but mixing them in a single project creates a cognitive load for anyone else reading your code. Pick one (I personally lean toward AnyFlatSpec for its balance of brevity and clarity) and be consistent.




📋 Practical Task

Test Suite for a Shopping Cart Validator

You are tasked with testing a CartValidator class. The validator ensures that a shopping cart meets certain business rules before it can be checked out.

The Requirements:

  • The cart cannot be empty.
  • The total price must be greater than $0.
  • If the cart contains a "Hazardous" item, it must have a "Safety Permit" item included.

Your Task:

  1. Create a CartItem case class (with name: String and price: Double).
  2. Implement a CartValidator class with a method validate(items: List[CartItem]): Either[String, Boolean]. It should return Left("Error Message") if a rule is broken, or Right(true) if the cart is valid.
  3. Write a ScalaTest suite using AnyFlatSpec and Matchers that covers:
    • A successful validation of a normal cart.
    • A failure when the cart is empty.
    • A failure when a "Hazardous" item is present without a "Safety Permit".
    • A success when both "Hazardous" and "Safety Permit" items are present.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.