Как тестировать функции в Scala с использованием ScalaTest?

Для тестирования функций в Scala одним из самых популярных инструментов является ScalaTest — мощная и гибкая библиотека, которая поддерживает несколько стилей написания тестов. Она позволяет создавать модульные, интеграционные и property-based тесты. ScalaTest предоставляет разнообразные синтаксические DSL’ы: FunSuite, FlatSpec, WordSpec, FreeSpec, PropSpec, FeatureSpec, а также AnyFunSuite, AnyFlatSpec и другие, начиная с ScalaTest 3.1.0+.

Установка ScalaTest

Для использования ScalaTest с sbt в build.sbt добавляется зависимость:

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.18" % Test

Пример функции для тестирования

object MathUtils {
def add(a: Int, b: Int): Int = a + b
def divide(a: Int, b: Int): Either\[String, Int\] =
if (b == 0) Left("division by zero")
else Right(a / b)
}

Пример тестов с использованием AnyFunSuite

import org.scalatest.funsuite.AnyFunSuite
class MathUtilsTest extends AnyFunSuite {
test("add should return the sum of two integers") {
assert(MathUtils.add(2, 3) === 5)
assert(MathUtils.add(-1, 1) === 0)
}
test("divide should return Right result for valid division") {
assert(MathUtils.divide(10, 2) === Right(5))
}
test("divide should return Left for division by zero") {
assert(MathUtils.divide(10, 0) === Left("division by zero"))
}
}

Использование FlatSpec (читается как английский текст)

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class MathUtilsFlatSpec extends AnyFlatSpec with Matchers {
"add" should "return correct sum of two integers" in {
MathUtils.add(2, 3) shouldEqual 5
}
"divide" should "return Right when valid division" in {
MathUtils.divide(6, 2) shouldBe Right(3)
}
it should "return Left when division by zero" in {
MathUtils.divide(1, 0) shouldBe Left("division by zero")
}
}

Использование WordSpec

import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.should.Matchers
class MathUtilsWordSpec extends AnyWordSpec with Matchers {
"The add method" should {
"return the sum of two integers" in {
MathUtils.add(10, 5) shouldEqual 15
}
}
"The divide method" should {
"return Right for valid input" in {
MathUtils.divide(8, 2) shouldBe Right(4)
}
"return Left when dividing by zero" in {
MathUtils.divide(8, 0) shouldBe Left("division by zero")
}
}
}

Проверки: assert, should, must, expect

ScalaTest предоставляет множество стилей утверждений:

  • assert(condition)

  • assertResult(expected)(actual)

  • shouldBe, shouldEqual, should not be

  • mustBe, mustEqual — через MustMatchers (устарело, не рекомендуется)

assert(MathUtils.add(2, 2) == 4)
assertResult(4)(MathUtils.add(2, 2))
MathUtils.add(2, 2) shouldBe 4

Проверка выбрасывания исключений

test("division by zero should throw ArithmeticException") {
assertThrows\[ArithmeticException\] {
val x = 1 / 0
}
}

Или:

intercept\[ArithmeticException\] {
val x = 1 / 0
}

Тестирование с TableDrivenPropertyChecks (табличные тесты)

import org.scalatest.prop.TableDrivenPropertyChecks._
import org.scalatest.funsuite.AnyFunSuite
class MathTableTest extends AnyFunSuite {
val inputs = Table(
("a", "b", "expected"),
(1, 2, 3),
(3, 4, 7),
(0, 0, 0)
)
forAll(inputs) { (a: Int, b: Int, expected: Int) =>
assert(MathUtils.add(a, b) === expected)
}
}

Моки с помощью Mockito или ScalaMock

libraryDependencies += "org.scalatestplus" %% "mockito-4-11" % "3.2.18.0" % Test
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._
import org.scalatest.funsuite.AnyFunSuite
class UserServiceTest extends AnyFunSuite with MockitoSugar {
test("getUser should call UserRepository") {
val mockRepo = mock\[UserRepository\]
val service = new UserService(mockRepo)
when(mockRepo.findUser("bob")).thenReturn(Some(User("bob")))
val result = service.getUser("bob")
assert(result.contains(User("bob")))
}
}

Организация и запуск тестов

  • Все тесты автоматически обнаруживаются sbt при запуске команды:
sbt test
  • Для запуска конкретного теста:
sbt "testOnly \*MathUtilsTest"

Асинхронные тесты с AsyncFunSuite

Для тестирования Future:

import org.scalatest.funsuite.AsyncFunSuite
import scala.concurrent.Future
class FutureTest extends AsyncFunSuite {
test("async test") {
Future {
1 + 1
}.map(result => assert(result == 2))
}
}

Советы

  • Используй Matchers для читаемости (shouldBe, shouldEqual).

  • Делай тесты изолированными, не зависящими от других.

  • Покрывай граничные случаи.

  • Вынеси повторяющийся код в beforeAll, beforeEach, Fixture-тесты.

  • Запускай тесты в CI: sbt test в GitHub Actions или других пайплайнах.