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.
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.
- A method stub should always be executed, as the real method would.
- An unstubbed method must throw and not return a lenient sentinel value.
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"))
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)
No. Smockito leverages a handful of powerful Scala 3 features, such as inlining, opaque types, context functions and match types. 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.
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.
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(IllegalArgumentException("Unexpected user"))
Notice we are handling partiality explicitly. This is useful if you don't want Smockito to throw UnexpectedArguments
behind the scenes.
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.
Don't. Instead of clearing history on a global mock, create a fresh mock for each test case. This approach avoids race conditions entirely, with a negligible performance cost.
No. It's always best to define a unique stub and be explicit about behavior change. If you want to perform a different action on a subsequent invocation, for instance to simulate transient failures, consider using onCall
:
val repository =
mock[Repository[User]].onCall(it.exists):
case 1 | 2 => _ == "johndoe"
case _ => _ => false
If you have a mock whose setup is only slightly changed between test cases, instead of overriding a stub defined in some base trait, create a factory method:
def mockRepository(username: String): Mock[Repository[User]] =
mock[Repository[User]]
.on(() => it.get)(_ => List(User("johndoe")))
.on(it.exists)(_ == "johndoe")
.on(it.greet()(using _: User))(_ => s"Hello, $username!")
Yes. Use calledBefore
or calledAfter
:
val repository =
mock[Repository[User]]
.on(it.exists)(_ => true)
.on(() => it.get)(_ => List.empty)
val _ = repository.exists("johndoe")
val _ = repository.get
assert(repository.calledBefore(it.exists, () => it.get))
When doing so, consider whether this behavior is a hard requirement of your system or merely an implementation detail. If it is the latter, the assertion might be an overspecification.
An unstubbed method call will throw an UnstubbedMethod
exception. This decision is based on the belief that returning a lenient value would reduce test readability and increase the likelihood of bugs.
Even so, you may want to dispatch an adapter method to its actual implementation in order to stub a method at the bottom of the hierarchy. This can be achieved using real
:
trait Getter:
def getNames: List[String]
def getNamesAdapter(setting: String) = getNames
val getter = mock[Getter]
.on(() => it.getNames)(_ => List("john"))
.real(it.getNamesAdapter)
assert(getter.getNamesAdapter("dummy") == List("john"))
assert(getter.times(() => it.getNames) == 1)
The last stub takes precedence. If possible, follow the unique stub principle.
Are you performing eta-expansion correctly? Check out the main SmockitoSpec for more examples covering a variety of situations. If everything looks fine on your side, please file an issue with a minimal reproducible example.
Mostly anything you want to. Check the license. All contributions are appreciated.