Как использовать mocking в Scala (например, с помощью Mockito или ScalaMock)?
Мокинг (mocking) — это техника подмены реальных зависимостей фиктивными объектами (моками) для изоляции логики, которую нужно протестировать. В Scala для мокинга чаще всего используют библиотеки Mockito (адаптированная через mockito-scala) и ScalaMock. Обе позволяют перехватывать вызовы методов, задавать их поведение и проверять, были ли они вызваны с нужными аргументами.
Mockito в Scala
Для использования Mockito в Scala чаще применяют обёртку mockito-scala, которая делает синтаксис более «scala-образным».
Подключение:
libraryDependencies += "org.mockito" %% "mockito-scala-scalatest" % "1.17.14" % Test
Пример мокинга интерфейса:
import org.mockito.MockitoSugar
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
trait UserRepository {
def findUser(id: Int): Option\[String\]
}
class UserService(repo: UserRepository) {
def greet(id: Int): String =
repo.findUser(id).map(name => s"Hello, $name!").getOrElse("User not found")
}
class UserServiceSpec extends AnyFlatSpec with Matchers with MockitoSugar {
"UserService" should "greet existing user" in {
val mockRepo = mock\[UserRepository\]
when(mockRepo.findUser(1)).thenReturn(Some("Alice"))
val service = new UserService(mockRepo)
service.greet(1) shouldBe "Hello, Alice!"
}
it should "handle missing user" in {
val mockRepo = mock\[UserRepository\]
when(mockRepo.findUser(2)).thenReturn(None)
val service = new UserService(mockRepo)
service.greet(2) shouldBe "User not found"
}
}
Проверка вызовов:
verify(mockRepo).findUser(1)
verify(mockRepo, never).findUser(999)
ScalaMock
ScalaMock — библиотека мокинга, написанная специально для Scala. Она лучше поддерживает мокинг траитов, классов, методов с by-name параметрами и currying.
Подключение:
libraryDependencies += "org.scalamock" %% "scalamock" % "5.2.0" % Test
Пример с использованием MockFactory:
import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec
trait Calculator {
def add(x: Int, y: Int): Int
}
class MathService(calc: Calculator) {
def doubleSum(a: Int, b: Int): Int = calc.add(a, b) \* 2
}
class MathServiceSpec extends AnyFlatSpec with MockFactory {
"MathService" should "call add and double result" in {
val mockCalc = mock\[Calculator\]
(mockCalc.add \_).expects(3, 4).returning(7)
val service = new MathService(mockCalc)
assert(service.doubleSum(3, 4) == 14)
}
}
Возможности ScalaMock:
Проверка количества вызовов:
(mockCalc.add \_).expects(3, 4).once()
Ожидания с матчерами:
(mockCalc.add \_).expects(\*, \*).returning(42)
Мокинг асинхронных методов
Моки часто используются для подмены сервисов, возвращающих Future, IO, ZIO.
Пример для Future с Mockito:
trait AsyncService {
def compute(x: Int): Future\[Int\]
}
val mockService = mock\[AsyncService\]
when(mockService.compute(10)).thenReturn(Future.successful(100))
С IO:
trait EffectService {
def getValue: IO\[Int\]
}
val mockService = mock\[EffectService\]
when(mockService.getValue).thenReturn(IO.pure(42))
Мокинг классов с параметрами
Mockito и ScalaMock позволяют мокать классы с зависимостями, но только если они не final. В Scala 3 или при строгих настройках компилятора нужно помечать классы как open (или использовать -Ydelambdafy:inline и -language:experimental.macros для ScalaMock).
Ограничения
-
Mockito:
-
Требует не-final классы и методы.
-
Не работает с val-ами напрямую.
-
Вариативно поддерживает currying.
-
-
ScalaMock:
-
Лучше работает с trait’ами и функциями с сложными сигнатурами.
-
Медленнее в больших проектах (в compile-time).
-
Может быть сложнее в синтаксисе для новичков.
-
Дополнительные техники
Использование stub в ScalaMock для упрощённых моков:
val stubRepo = stub\[UserRepository\]
(stubRepo.findUser \_).when(1).returns(Some("Mocked"))
- Создание Fake вручную вместо мока, если нужно простое поведение без проверок вызовов.
Моки с конструктором через Cake Pattern или DI
Если зависимость передаётся через конструктор, мок вставляется напрямую. Это позволяет эффективно тестировать с изоляцией:
class Service(dependency: MyTrait)
val mock = mock\[MyTrait\]
val service = new Service(mock)
Если используется DI-фреймворк (MacWire, Guice, ZIO Layers), мок можно внедрить на уровне конфигурации или окружения.
Сброс состояния мока
Для многократного использования можно сбрасывать моки:
reset(mock)
Или использовать clearInvocations(mock) — если нужно сохранить поведение, но очистить историю вызовов.
Тайминг и последовательность
Можно задать последовательность:
val inOrder = inOrder(mockA, mockB)
inOrder.verify(mockA).firstMethod()
inOrder.verify(mockB).secondMethod()
Это полезно, если порядок вызовов критичен, например, при работе с ресурсами или логикой транзакций.
Мокинг в Scala — мощный инструмент при тестировании, особенно при изоляции от внешних сервисов, работы с сетью, БД или асинхронными API. Выбор между ScalaMock и Mockito зависит от предпочтений и сложности сигнатур, но обе библиотеки хорошо подходят для задач мокинга в Scala.