Mockito-Scala: Mocking in Scala Tests

Mockito-Scala: Mocking in Scala Tests

Testing code that depends on external services, databases, or complex collaborators requires isolation — you want to test one unit at a time. Mockito-Scala brings Mockito's mocking capabilities to Scala with an idiomatic API that handles Scala-specific features like default arguments, final classes, and type inference.

Why Mockito-Scala Over Plain Mockito?

Java's Mockito works in Scala but has friction points:

  • when(mock.method()) doesn't work well with Scala's type inference
  • Scala default parameters cause issues
  • any() matchers return null instead of default values, causing NullPointerExceptions
  • Verifying multiple calls is verbose

Mockito-Scala wraps Mockito with a Scala-friendly API that solves all of these.

Installation

libraryDependencies ++= Seq(
  "org.mockito" %% "mockito-scala" % "1.17.37" % Test,
  // Optional ScalaTest integration
  "org.mockito" %% "mockito-scala-scalatest" % "1.17.37" % Test
)

Creating Mocks

import org.mockito.MockitoSugar

class UserServiceTest extends AnyFlatSpec with MockitoSugar with Matchers {

  // Create a mock
  val userRepo: UserRepository = mock[UserRepository]
  val emailService: EmailService = mock[EmailService]

  // Create service with mocked dependencies
  val userService = new UserService(userRepo, emailService)
}

MockitoSugar is the main entry point for mock creation and stubbing.

Stubbing Method Returns

// Return a value
when(userRepo.findById(42)) thenReturn Some(User(42, "Alice"))

// Return None
when(userRepo.findById(99)) thenReturn None

// Throw an exception
when(userRepo.findById(-1)) thenThrow new IllegalArgumentException("negative id")

// Return different values on consecutive calls
when(userRepo.nextId())
  .thenReturn(1)
  .thenReturn(2)
  .thenReturn(3)

// Use Answer for complex logic
when(userRepo.save(any[User])) thenAnswer { invocation =>
  val user = invocation.getArgument[User](0)
  user.copy(id = Some(generateId()))
}

Argument Matchers

import org.mockito.ArgumentMatchers._

// Match any argument
when(userRepo.findById(any[Int])) thenReturn None

// Match specific values
when(userRepo.findByEmail(eqTo("alice@example.com"))) thenReturn Some(user)

// Match by predicate
when(userRepo.findByAge(argThat((n: Int) => n >= 18))) thenReturn List(user)

// String matchers
when(userRepo.search(startsWith("Al"))) thenReturn List(alice)
when(userRepo.search(contains("@"))) thenReturn List(alice, bob)

Important: When using matchers, ALL arguments must use matchers (or eqTo for specific values):

// Wrong — mixing literal and matcher
when(service.method(42, any[String])) thenReturn "result"

// Correct
when(service.method(eqTo(42), any[String])) thenReturn "result"

Verifying Interactions

After exercising your code, verify mocks were called as expected:

test("creates user and sends welcome email") {
  when(userRepo.save(any[User])) thenReturn User(1, "Alice", "alice@example.com")

  userService.register("Alice", "alice@example.com")

  // Verify called once
  verify(userRepo).save(User(0, "Alice", "alice@example.com"))
  verify(emailService).sendWelcome("alice@example.com")
}

// Verify call count
verify(userRepo, times(3)).findById(any[Int])
verify(emailService, atLeastOnce).sendWelcome(any[String])
verify(emailService, never).sendAlert(any[String])

// Verify no interactions
verifyNoInteractions(emailService)
verifyNoMoreInteractions(userRepo)

Captor — Capturing Arguments

Capture arguments to make detailed assertions:

import org.mockito.ArgumentCaptor

test("passes correct user to repository") {
  val userCaptor = ArgumentCaptor.forClass(classOf[User])

  userService.register("Alice", "alice@example.com", role = "admin")

  verify(userRepo).save(userCaptor.capture())
  val savedUser = userCaptor.getValue
  savedUser.name should be("Alice")
  savedUser.role should be("admin")
  savedUser.createdAt should not be null
}

With Mockito-Scala's idiomatic API:

import org.mockito.captor.ArgCaptor

val captor = ArgCaptor[User]
verify(userRepo).save(captor)
captor.value.name should be("Alice")

ScalaTest Integration

The IdiomaticMockito trait provides a cleaner, more Scala-like syntax:

import org.mockito.scalatest.IdiomaticMockito

class UserServiceSpec extends AnyFlatSpec
    with Matchers
    with IdiomaticMockito {

  "UserService.register" should "save user and send email" in {
    val userRepo = mock[UserRepository]
    val emailService = mock[EmailService]
    val service = new UserService(userRepo, emailService)

    // Idiomatic stubbing
    userRepo.save(*) returns User(1, "Alice", "alice@example.com")
    emailService.sendWelcome(*) doesNothing()

    service.register("Alice", "alice@example.com")

    // Idiomatic verification
    userRepo.save(*) wasCalled once
    emailService.sendWelcome("alice@example.com") wasCalled once
  }
}

The * wildcard is Mockito-Scala's idiomatic alternative to any().

Mocking Final Classes and Objects

Scala classes are final by default. Mockito-Scala handles this with inline mocking (requires no ByteBuddy configuration on newer JVMs):

// In test/resources/mockito-extensions/org.mockito.plugins.MockMaker
// Add: mock-maker-inline

// Then in tests
val finalClass = mock[MyFinalClass]
when(finalClass.method()) thenReturn "result"

Or use the @MockitoSettings annotation:

@MockitoSettings(strictness = Strictness.LENIENT)
class MyTest extends AnyFlatSpec with MockitoSugar { ... }

Spies — Partial Mocking

A spy wraps a real object, letting you stub specific methods while using the real implementation for others:

val realService = new UserService(realRepo, realEmailer)
val spy = spyLambda(realService)  // or spy(realService) for non-final classes

// Only stub the slow external call
when(spy.fetchExternalProfile(any[String])) thenReturn Profile.default

// Everything else uses the real implementation
val user = spy.register("Alice", "alice@example.com")
user.name should be("Alice")  // Real logic ran
verify(spy).fetchExternalProfile("alice@example.com")  // Stubbed method was called

ReturnsDeepStubs

For chained method calls (use sparingly — it often signals a design smell):

val config = mock[AppConfig](ReturnsDeepStubs)
when(config.database.primary.host) thenReturn "localhost"
when(config.database.primary.port) thenReturn 5432

Reset Mocks Between Tests

If sharing mocks across tests:

afterEach {
  reset(userRepo, emailService)
}

Or better, create fresh mocks in each test to avoid state leakage.

Common Mistakes

Stubbing after the call: Always stub before the code under test runs.

Mixing matchers and literals: Use eqTo() when combining with other matchers.

Verifying what you stubbed: If you stubbed findById(42) and verify findById(42), you've proved nothing — you just confirmed Mockito tracked the call. Verify interactions your code should make, not interactions that are forced by stubs.

Over-mocking: Don't mock value objects, data classes, or simple utilities. Mock boundary objects — repositories, HTTP clients, email services.

End-to-End Complement

Mockito-Scala isolates units for fast, focused tests. For verifying that your Scala application's user-facing features work end-to-end — with real HTTP, real rendering, and real browser interactions — HelpMeTest provides continuous browser monitoring. Unit mocking tests logic correctness; browser testing tests user experience.

Summary

Mockito-Scala makes mocking idiomatic in Scala:

  • mock[T] creates mocks for traits and classes
  • when(...) thenReturn stubs behavior
  • verify(mock).method(args) confirms interactions
  • IdiomaticMockito provides cleaner syntax with returns, wasCalled
  • Argument captors inspect what was passed to collaborators
  • Spies partially mock real objects

Keep mocks focused on boundary objects, keep stubs minimal, and verify only the interactions your design requires — that's the recipe for maintainable test suites.

Read more