Announcing ScalaMock 7

Introduction

Paul Butcher started this great project long time ago, but unfortunately he left Scala. For a long time it was maintained by Philipp Meyerhoefer, but unfortunately he left too, so in the beginning of 2024 ScalaMock was almost abandoned, but now it is not.

Before I continue, I want to express my gratitudes to Paul, Philipp and other related people for creation of ScalaMock. It was very useful to me these 5 years that I’m using Scala.


ScalaMock 6

We published ScalaMock 6 at the beginning of 2024. It was a release focused on a smooth migration from scala 2 to scala 3, and cross-compiled to 2.12, 2.13, and 3. It also used java 8.
I implemented the most part and Jan Chyb implemented the hardest - generics support.

Since I have no prior experience in scala 2 metaprogramming - scala 2 support is mostly dropped, but still some bug fixes not related to metaprogramming can be done.

Some fixes related to scala 3 metaprogramming will be backported to ScalaMock 6 if they were fixed in ScalaMock 7.

Organizational moments

This year we:

  1. Created ScalaMock organization on github, transferred project there from Paul personal account and transferred ownership to me also
  2. Set up github CI release pipeline

And now it is time to move on.


Road to ScalaMock 7

I have used mocks and stubs for years and it was really painful to use them, but even more painful was to not use them at all.

In the middle of 2024 my work project started migration from Future to ZIO and at the end of 2024 we have finally migrated to scala 3 also.

And we met few issues:

  1. ScalaMock can’t work with ZIO properly
  2. We tried zio-mock and I really appreciate what the author tried to achieve, but for me personally using it was a mess, especially without @mockable which was not ported to scala 3

That time I started to write stubs by hand. And I needed only 2 things:

  1. To set expected result (sometimes based on arguments)
  2. Get arguments with which method was invoked to verify them

It was tedious, but it worked.

At some point I’ve got tired to write them by hand and started to think how to make it generic. Also I wanted to use that new and shiny TupledFunction somehow to generalize method selection. So given some experience in rewriting ScalaMock I achieved that in a project named backstub, which essentially became a draft for ScalaMock 7 being very different at the same time. It helped me to shape how I want it to look like in general and I got some feedback from my colleagues.


ScalaMock 7

Some people say never use mocks and I agree with them now, but not in general. Mocks are evil, because they check expectations on the go and the only way to report failure there - throwing an exception. And throwing an exception not scales, only adds more complexity.

But stubs on the other side verify everything after. So here we may not throw an exception, if we could just collect method arguments and check them.

This is the core idea behind new ScalaMock 7 experimental API. Let’s see how well it scales.

Consider some simplified example of user authentication by password.

enum UserStatus:
  case Normal, Blocked

enum AuthResult:
  case Success, UserNotFound, UserNotAllowed, WrongPassword

case class User(id: Long, status: UserStatus)

trait UserService:
  def findUser(userId: Long): Option[User]

trait PasswordService:
  def checkPassword(id: Long, password: String): Boolean

class UserAuthService(
   userService: UserService,
   passwordService: PasswordService
):
  def authorize(id: Long, password: Password): AuthResult =
    userService.findUser(id) match
      case None =>
        AuthResult.UserNotFound

      case Some(user) if user.status == UserStatus.Blocked => 
        AuthResult.UserNotAllowed

      case Some(user) if passwordService.checkPassword(user.id, password) =>
        AuthResult.WrongPassword
       
      case _ =>
        AuthResult.Success
      

Where you need to test UserAuthService with scalatest

import org.scalatest.anyfun.AnyFunSpec
import org.scalatest.matchers.should.Matchers
import org.scalamock.stubs.{Stubs, CallLog}

class UserAuthServiceSpec extends AnyFunSpec, Matchers, Stubs:
  val unknownUserId = 0
  val user = User(1, UserStatus.Normal)
  val blockedUser = User(2, UserStatus.Blocked)
  val validPassword = "valid"
  val invalidPassword = "invalid"

  it("success if user not blocked and password matches"):
    given CallLog = CallLog() // this is optional, only if you want to verify order

    val userService = stub[UserService]
    val passwordService = stub[PasswordService]
    val authUserService = AuthUserService(userService, passwordService)
    
    userService.findUser.returns(_ => Some(user))
    passwordService.checkPassword.returns(_ =>  true)
   
    authUserService.authorize(user.id, validPassword) shouldBe AuthResult.Success

    // we can also make some verifications (note that here it is not needed)

    // userService.findUser was called one time
    userService.findUser.times shouldBe 1

    // userService.findUser was called with not blocked user id
    userService.findUser.calls shouldBe List(user.id)

   // userService.findUser was called before passwordService.checkPassword
   userService.findUser.isBefore(passwordService.checkPassword) shouldBe true
    

And this is only one test-case, but we need 4, let’s try generalizing it.


case class Verify(
  timesPasswordChecked: Option[Int] = None
)

def testCase(
  description: String,
  userId: Long,
  password: String,
  result: AuthResult
  verify: Verify = Verify()
): Unit =
 it(description):
    val userService = stub[UserService]
    val passwordService = stub[PasswordService]
    val authUserService = AuthUserService(userService, passwordService)
    
    userService.findUser.returns:
      case user.id => Some(user)
      case blockedUser.id => Some(blockedUser)
      case _ => None
  
    passwordService.checkPassword.returns:
      case `validPassword` => true
      case _ => false

    authUserService.authenticate(userId, password) shouldBe result
  
    verify.timesPasswordCheck.
      .foreach(expected => passwordService.checkPassword.times shouldBe expected)
   


Now our test cases look simple enough:

testCase(
  description = "fail if user not found",
  userId = unknownUserId,
  password = validPassword,
  result = AuthResult.UserNotFound,
  verify = Verify(timesPasswordChecked = Some(0))
)

testCase(
  description = "fail if user is blocked",
  userId = blockedUser.id,
  password = validPassword,
  result = AuthResult.UserNotAllowed,
  verify = Verify(timesPasswordChecked = Some(0))
)

testCase(
  description = "fail if password not match",
  userId = user.id,
  password = invalidPassword,
  result = AuthResult.WrongPassword,
  verify = Verify(timesPasswordChecked = Some(1))
)

testCase(
  description = "happy path",
  userId = user.id,
  password = validPassword,
  result = AuthResult.Success,
  verify = Verify(timesPasswordChecked = Some(1))
)

In case you enjoyed it as much as I did

Setup is:

scalaVersion := "3.4.3" // or higher
Test / scalacOptions += "-experimental"

libraryDependencies += "org.scalamock" %% "scalamock" % "7.1.0"

For ZIO users everything is same, but you should mixin ZIOStubs and use returnsZIO method.

libraryDependencies += "org.scalamock" %% "scalamock-zio" % "7.1.0"

For cats-effect users everything is same, but you should mixin CatsEffectStubs and use returnsIO method.

libraryDependencies += "org.scalamock" %% "scalamock-cats-effect" % "7.1.0"

The End?

That’s all for now, if you have any thoughts on how to make it better - see you in ScalaMock issues and discussions.

Georgii Kovalev

11 Likes