bdmendes / smockito   1.2.5

MIT License GitHub

Tiny Scala facade for Mockito.

Scala versions: 3.x

Smockito

Build Codecov Maven Central Javadoc

Smockito is a tiny framework-agnostic Scala 3 facade for Mockito. It enables setting up unique method and value stubs for any type in a type-safe manner, while providing an expressive interface for inspecting received arguments and call counts.


Motivation

Even when software components make use of proper dependency injection, a mocking framework is useful as a construction and interception sugar. scalamock is an excellent native tool for that use case, but has its limitations. Mockito, on the other hand, is very powerful and popular, but exposes a Java API that arguably does not fit well with Scala's expressiveness and safety.

Smockito leverages a subset of Mockito’s features and offers a minimal, opinionated interface, guided by a few core principles:

  • A method should be stubbed only once, and use the same implementation for the lifetime of the mock.
  • A method stub should handle only the inputs it expects; errors should be handled by the framework.
  • A method stub should always be executed, as the real method would.
  • One may reason directly about the received arguments and number of calls of a stub.
  • One should not reason about the history of a method that was not stubbed.

Quick Start

To use Smockito in an existing sbt project with Scala 3, add the following dependency to your build.sbt:

libraryDependencies += "com.bdmendes" %% "smockito" % "<version>" % Test

Do not depend on Mockito directly.

If targeting Java 24+, you need to add the Smockito JAR as a Java agent to enable the runtime bytecode manipulation that Mockito depends on. If you use the sbt-javaagent plugin, you can simply add to your build.sbt:

javaAgents += "com.bdmendes" % "smockito_3" % "<version>" % Test

In your specification, extend Smockito. This will bring the mock method and relevant conversions to scope. To set up a mock, add stub definitions with the on method, which requires an eta-expanded method reference, that you may easily express with it, and a partial function to handle the relevant inputs.

abstract class Repository[T](val name: String):
  def get: List[T]
  def exists(username: String): Boolean
  def greet()(using T): String
  def getWith(startsWith: String, endsWith: String): List[T]

case class User(username: String)

class RepositorySpecification extends Smockito:
  val repository = mock[Repository[User]]
    .on(() => it.name)(_ => "xpto")
    .on(() => it.get)(_ => List(User("johndoe")))
    .on(it.exists)(_ == "johndoe")
    .on(it.greet()(using _: User))(user => s"Hello, ${user.username}!")
    .on(it.getWith) { 
      case ("john", name) if name.nonEmpty => List(User("johndoe"))
    } // Mock[Repository[User]]

A Mock[T] is a T both at compile time and runtime.

  assert(repository.getWith("john", "doe") == List(User("johndoe")))

You may reason about method interactions with calls and times. If arguments are not needed, times is more efficient.

  assert(repository.calls(it.getWith) == List(("john", "doe")))
  assert(repository.times(it.getWith) == 1)

FAQ

Does Smockito support Scala 2?

No. Smockito leverages a handful of powerful Scala 3 features, such as inlining, opaque types and contextual functions. If you are on the process of migrating a Scala 2 codebase, it might be a good opportunity to replace the likes of specs2-mock or mockito-scala as you migrate your modules.

Is this really a mocking framework?

This is a facade for Mockito, which in itself is technically a test spy framework. There is a great debate regarding the definitions of mocks, stubs, spies, test duplicates... Here, we assume a mock to be a "faked" object, and a stub a provided implementation for a subset of the input space.

Is Smockito thread-safe?

As thread-safe as Mockito.

How do I spy on a real instance?

Though not the main Smockito use case, you may achieve so by setting up a stub on a mock that forwards to a real instance:

val repository = {
  val realInstance = Repository.fromDatabase[User]
  mock[Repository[User]].forward(it.exists, realInstance)
}

assert(repository.times(it.exists) == 0)

That said, make sure you also test the real instance in isolation.

Is Smockito compatible with effect systems?

Yes. Implement your stub as you would in application code. For example, with cats-effect:

abstract class EffectRepository[T]:
  def exists(username: String): IO[Boolean]

val repository =
  mock[EffectRepository[User]].on(it.exists) {
    case "johndoe" =>
      IO(true)
    case _ =>
      IO.raiseError(new IllegalArgumentException("Unexpected user"))
  }

Notice we are handling partiality explicitly. This is useful if you don't want Smockito to throw UnexpectedArguments behind the scenes.

I need to override stubs or reason about unstubbed methods.

If you are in the process of migrating from another mocking framework and stumble across Smockito's opinionated soundness verifications, you might be interested in disabling them via the trait constructor:

trait MySpec extends Smockito(SmockitoMode.Relaxed)

I need to assert invocation orders/X/Y/Z.

You may fall back to the Mockito API anytime you see fit; a Mock[T] may be passed safely. Smockito wants to be as small as possible, but if there is an interesting new use case you'd want to see handled here, please open an issue.

I can't seem to stub a method/I found a bug.

Are you performing eta-expansion correctly? If everything looks fine on your side, please file an issue with a minimal reproducible example.

What can I do with the source code?

Mostly anything you want to. Check the license. All contributions are appreciated.

Special Thanks

  • Mockito: for the reliable core.
  • Scalamock: for the excellent Stubs API design that inspired this library.
  • @biromiro: for designing the cute cocktail logo.