Advanced Testing Patterns: Property-Based Testing, Mocking, and Integration Strategies

Comprehensive testing is crucial for building reliable Scala applications. This lesson explores advanced testing techniques including property-based testing, sophisticated mocking strategies, integration testing patterns, and test automation frameworks that ensure code quality and system reliability.

Property-Based Testing with ScalaCheck

Understanding Property-Based Testing Fundamentals

import org.scalacheck._
import org.scalacheck.Prop._
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks

// Basic property-based testing examples
class PropertyBasedTestingExamples extends AnyFlatSpec with Matchers with ScalaCheckPropertyChecks {

  // Simple property: reversing a list twice returns the original list
  "List reverse property" should "satisfy double reverse identity" in {
    forAll { (list: List[Int]) =>
      list.reverse.reverse shouldEqual list
    }
  }

  // Mathematical properties
  "Addition properties" should "be commutative and associative" in {
    forAll { (a: Int, b: Int, c: Int) =>
      whenever(a < Int.MaxValue - b && b < Int.MaxValue - c && a < Int.MaxValue - c) {
        // Commutativity
        a + b shouldEqual b + a

        // Associativity
        (a + b) + c shouldEqual a + (b + c)

        // Identity element
        a + 0 shouldEqual a
      }
    }
  }

  // String properties
  "String concatenation" should "preserve length" in {
    forAll { (s1: String, s2: String) =>
      (s1 + s2).length shouldEqual s1.length + s2.length
    }
  }

  // Collection properties
  "Map operations" should "maintain invariants" in {
    forAll { (map: Map[String, Int], key: String, value: Int) =>
      val updatedMap = map + (key -> value)

      // Size property
      if (map.contains(key)) {
        updatedMap.size shouldEqual map.size
      } else {
        updatedMap.size shouldEqual map.size + 1
      }

      // Contains property
      updatedMap should contain key -> value

      // All original keys (except possibly the updated one) should remain
      map.keys.filterNot(_ == key).foreach { k =>
        updatedMap should contain key k
        updatedMap(k) shouldEqual map(k)
      }
    }
  }
}

// Custom generators for domain objects
object CustomGenerators {

  // Generator for email addresses
  val emailGen: Gen[String] = for {
    username <- Gen.alphaNumStr.suchThat(_.nonEmpty)
    domain <- Gen.oneOf("gmail.com", "yahoo.com", "company.com", "example.org")
  } yield s"$username@$domain"

  // Generator for valid phone numbers
  val phoneGen: Gen[String] = for {
    areaCode <- Gen.choose(100, 999)
    exchange <- Gen.choose(100, 999)
    number <- Gen.choose(1000, 9999)
  } yield f"$areaCode-$exchange-$number"

  // Generator for business domain objects
  case class User(id: Long, name: String, email: String, age: Int)

  val userGen: Gen[User] = for {
    id <- Gen.posNum[Long]
    name <- Gen.alphaStr.suchThat(_.length > 2)
    email <- emailGen
    age <- Gen.choose(18, 100)
  } yield User(id, name, email, age)

  // Generator for complex data structures
  case class Order(id: String, userId: Long, items: List[OrderItem], total: BigDecimal)
  case class OrderItem(productId: String, quantity: Int, price: BigDecimal)

  val orderItemGen: Gen[OrderItem] = for {
    productId <- Gen.uuid.map(_.toString)
    quantity <- Gen.choose(1, 10)
    price <- Gen.choose(1.0, 1000.0).map(BigDecimal(_))
  } yield OrderItem(productId, quantity, price)

  val orderGen: Gen[Order] = for {
    id <- Gen.uuid.map(_.toString)
    userId <- Gen.posNum[Long]
    items <- Gen.listOfN(Gen.choose(1, 5).sample.get, orderItemGen)
    total = items.map(item => item.price * item.quantity).sum
  } yield Order(id, userId, items, total)

  // Generator with constraints
  val validOrderGen: Gen[Order] = orderGen.suchThat { order =>
    order.items.nonEmpty &&
    order.total > 0 &&
    order.items.forall(_.quantity > 0) &&
    order.items.forall(_.price > 0)
  }

  // Frequency-based generators
  val statusGen: Gen[String] = Gen.frequency(
    (70, "ACTIVE"),
    (20, "INACTIVE"),
    (9, "PENDING"),
    (1, "SUSPENDED")
  )

  // Recursive generators for tree structures
  sealed trait Tree[T]
  case class Leaf[T](value: T) extends Tree[T]
  case class Branch[T](left: Tree[T], right: Tree[T]) extends Tree[T]

  def treeGen[T](valueGen: Gen[T], maxDepth: Int): Gen[Tree[T]] = {
    def treeGenHelper(depth: Int): Gen[Tree[T]] = {
      if (depth >= maxDepth) {
        valueGen.map(Leaf(_))
      } else {
        Gen.frequency(
          (1, valueGen.map(Leaf(_))),
          (3, for {
            left <- treeGenHelper(depth + 1)
            right <- treeGenHelper(depth + 1)
          } yield Branch(left, right))
        )
      }
    }
    treeGenHelper(0)
  }

  val intTreeGen: Gen[Tree[Int]] = treeGen(Gen.choose(-100, 100), 5)
}

// Advanced property testing patterns
class AdvancedPropertyTesting extends AnyFlatSpec with Matchers with ScalaCheckPropertyChecks {

  import CustomGenerators._

  // Testing with custom generators
  "User validation" should "accept valid users" in {
    forAll(userGen) { user =>
      validateUser(user) shouldBe true
    }
  }

  // Testing invariants
  "Order total calculation" should "equal sum of item totals" in {
    forAll(validOrderGen) { order =>
      val calculatedTotal = order.items.map(item => item.price * item.quantity).sum
      order.total shouldEqual calculatedTotal
    }
  }

  // Testing with preconditions
  "Division operation" should "be consistent" in {
    forAll { (a: Double, b: Double) =>
      whenever(b != 0.0 && !b.isInfinite && !b.isNaN && !a.isInfinite && !a.isNaN) {
        val result = a / b
        math.abs((result * b) - a) should be < 1e-10
      }
    }
  }

  // Testing state machines
  sealed trait BankAccountAction
  case class Deposit(amount: BigDecimal) extends BankAccountAction
  case class Withdraw(amount: BigDecimal) extends BankAccountAction
  case object CheckBalance extends BankAccountAction

  class BankAccount(private var balance: BigDecimal = 0) {
    def deposit(amount: BigDecimal): Either[String, BigDecimal] = {
      if (amount <= 0) Left("Amount must be positive")
      else {
        balance += amount
        Right(balance)
      }
    }

    def withdraw(amount: BigDecimal): Either[String, BigDecimal] = {
      if (amount <= 0) Left("Amount must be positive")
      else if (amount > balance) Left("Insufficient funds")
      else {
        balance -= amount
        Right(balance)
      }
    }

    def getBalance: BigDecimal = balance
  }

  val actionGen: Gen[BankAccountAction] = Gen.frequency(
    (40, Gen.choose(0.01, 1000.0).map(amount => Deposit(BigDecimal(amount)))),
    (30, Gen.choose(0.01, 500.0).map(amount => Withdraw(BigDecimal(amount)))),
    (30, Gen.const(CheckBalance))
  )

  "Bank account" should "maintain balance invariants" in {
    forAll(Gen.listOf(actionGen)) { actions =>
      val account = new BankAccount()
      var expectedBalance = BigDecimal(0)

      actions.foreach { action =>
        val balanceBefore = account.getBalance

        action match {
          case Deposit(amount) =>
            account.deposit(amount) match {
              case Right(newBalance) =>
                expectedBalance += amount
                newBalance shouldEqual expectedBalance
              case Left(_) =>
                amount should be <= 0
            }

          case Withdraw(amount) =>
            account.withdraw(amount) match {
              case Right(newBalance) =>
                expectedBalance -= amount
                newBalance shouldEqual expectedBalance
              case Left(_) =>
                // Either amount <= 0 or insufficient funds
                (amount <= 0 || amount > balanceBefore) shouldBe true
            }

          case CheckBalance =>
            account.getBalance shouldEqual expectedBalance
        }

        // Balance should never be negative
        account.getBalance should be >= 0
      }
    }
  }

  // Testing concurrent operations
  "Concurrent counter" should "maintain count accuracy" in {
    import scala.concurrent.{Future, ExecutionContext}
    import java.util.concurrent.atomic.AtomicInteger

    implicit val ec: ExecutionContext = ExecutionContext.global

    forAll(Gen.choose(1, 100)) { numOperations =>
      val counter = new AtomicInteger(0)
      val futures = (1 to numOperations).map { _ =>
        Future {
          counter.incrementAndGet()
        }
      }

      val result = Future.sequence(futures)
      result.futureValue.sum shouldEqual (numOperations * (numOperations + 1)) / 2
      counter.get() shouldEqual numOperations
    }
  }

  private def validateUser(user: User): Boolean = {
    user.name.nonEmpty &&
    user.email.contains("@") &&
    user.age >= 18 &&
    user.age <= 120
  }
}

// Performance property testing
class PerformancePropertyTesting extends AnyFlatSpec with Matchers with ScalaCheckPropertyChecks {

  "Sorting algorithms" should "have correct time complexity characteristics" in {
    forAll(Gen.choose(100, 10000)) { size =>
      val data = (1 to size).map(_ => scala.util.Random.nextInt(1000)).toList

      val startTime = System.nanoTime()
      val sorted = data.sorted
      val endTime = System.nanoTime()

      val duration = endTime - startTime

      // Verify correctness
      sorted shouldEqual data.sorted

      // Basic performance check (this is quite loose)
      // In practice, you'd use more sophisticated timing analysis
      duration should be < (size * size * 1000L) // Very loose upper bound
    }
  }

  "Map lookup performance" should "be roughly constant" in {
    forAll(Gen.choose(1000, 50000)) { size =>
      val map = (1 to size).map(i => i -> s"value$i").toMap
      val lookupKeys = (1 to 100).map(_ => scala.util.Random.nextInt(size) + 1)

      val startTime = System.nanoTime()
      lookupKeys.foreach(key => map.get(key))
      val endTime = System.nanoTime()

      val duration = endTime - startTime

      // Performance should be roughly constant regardless of map size
      // (This is a very simplified check)
      duration should be < 10000000L // 10ms for 100 lookups
    }
  }
}

Advanced Mocking and Test Doubles

// Mockito integration for Scala
import org.mockito.Mockito._
import org.mockito.ArgumentMatchers._
import org.mockito.{Mock, MockitoSugar}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfterEach
import scala.concurrent.{Future, ExecutionContext}
import scala.util.{Success, Failure}

// Service interfaces for testing
trait UserRepository {
  def findById(id: Long): Future[Option[User]]
  def save(user: User): Future[User]
  def delete(id: Long): Future[Boolean]
  def findByEmail(email: String): Future[Option[User]]
}

trait EmailService {
  def sendWelcomeEmail(user: User): Future[Boolean]
  def sendPasswordResetEmail(email: String, token: String): Future[Boolean]
}

trait AuditService {
  def logUserAction(userId: Long, action: String, details: Map[String, Any]): Future[Unit]
}

case class User(id: Long, name: String, email: String, age: Int, active: Boolean = true)

class UserService(
  userRepository: UserRepository,
  emailService: EmailService,
  auditService: AuditService
)(implicit ec: ExecutionContext) {

  def createUser(name: String, email: String, age: Int): Future[Either[String, User]] = {
    // Check if user already exists
    userRepository.findByEmail(email).flatMap {
      case Some(_) => Future.successful(Left("User with this email already exists"))
      case None =>
        if (age < 18) {
          Future.successful(Left("User must be at least 18 years old"))
        } else {
          val user = User(0, name, email, age)
          userRepository.save(user).flatMap { savedUser =>
            // Send welcome email
            emailService.sendWelcomeEmail(savedUser).flatMap { emailSent =>
              // Log the action
              auditService.logUserAction(savedUser.id, "USER_CREATED", Map(
                "email" -> email,
                "emailSent" -> emailSent
              )).map { _ =>
                Right(savedUser)
              }
            }
          }
        }
    }
  }

  def updateUser(id: Long, name: Option[String], email: Option[String]): Future[Either[String, User]] = {
    userRepository.findById(id).flatMap {
      case None => Future.successful(Left("User not found"))
      case Some(user) =>
        val updatedUser = user.copy(
          name = name.getOrElse(user.name),
          email = email.getOrElse(user.email)
        )

        userRepository.save(updatedUser).flatMap { saved =>
          auditService.logUserAction(id, "USER_UPDATED", Map(
            "oldName" -> user.name,
            "newName" -> saved.name,
            "oldEmail" -> user.email,
            "newEmail" -> saved.email
          )).map { _ =>
            Right(saved)
          }
        }
    }
  }

  def deactivateUser(id: Long): Future[Either[String, Unit]] = {
    userRepository.findById(id).flatMap {
      case None => Future.successful(Left("User not found"))
      case Some(user) if !user.active => Future.successful(Left("User already deactivated"))
      case Some(user) =>
        val deactivatedUser = user.copy(active = false)
        userRepository.save(deactivatedUser).flatMap { _ =>
          auditService.logUserAction(id, "USER_DEACTIVATED", Map(
            "email" -> user.email
          )).map { _ =>
            Right(())
          }
        }
    }
  }
}

// Comprehensive mocking tests
class UserServiceSpec extends AnyFlatSpec with Matchers with MockitoSugar with BeforeAndAfterEach {

  implicit val ec: ExecutionContext = ExecutionContext.global

  var userRepository: UserRepository = _
  var emailService: EmailService = _
  var auditService: AuditService = _
  var userService: UserService = _

  override def beforeEach(): Unit = {
    userRepository = mock[UserRepository]
    emailService = mock[EmailService]
    auditService = mock[AuditService]
    userService = new UserService(userRepository, emailService, auditService)
  }

  "UserService.createUser" should "create a new user successfully" in {
    // Setup mocks
    when(userRepository.findByEmail("john@example.com")).thenReturn(Future.successful(None))
    when(userRepository.save(any[User])).thenReturn(Future.successful(User(1, "John Doe", "john@example.com", 25)))
    when(emailService.sendWelcomeEmail(any[User])).thenReturn(Future.successful(true))
    when(auditService.logUserAction(any[Long], any[String], any[Map[String, Any]])).thenReturn(Future.successful(()))

    // Execute
    val result = userService.createUser("John Doe", "john@example.com", 25).futureValue

    // Verify
    result shouldBe a[Right[_, _]]
    result.right.get.name shouldEqual "John Doe"
    result.right.get.email shouldEqual "john@example.com"

    // Verify interactions
    verify(userRepository).findByEmail("john@example.com")
    verify(userRepository).save(any[User])
    verify(emailService).sendWelcomeEmail(any[User])
    verify(auditService).logUserAction(eq(1L), eq("USER_CREATED"), any[Map[String, Any]])
  }

  it should "reject users under 18" in {
    // Execute
    val result = userService.createUser("Young User", "young@example.com", 17).futureValue

    // Verify
    result shouldBe Left("User must be at least 18 years old")

    // Verify no interactions with dependencies for invalid input
    verify(userRepository, never()).save(any[User])
    verify(emailService, never()).sendWelcomeEmail(any[User])
  }

  it should "reject duplicate email addresses" in {
    // Setup
    val existingUser = User(1, "Existing User", "existing@example.com", 30)
    when(userRepository.findByEmail("existing@example.com")).thenReturn(Future.successful(Some(existingUser)))

    // Execute
    val result = userService.createUser("New User", "existing@example.com", 25).futureValue

    // Verify
    result shouldBe Left("User with this email already exists")

    verify(userRepository).findByEmail("existing@example.com")
    verify(userRepository, never()).save(any[User])
  }

  it should "handle email service failures gracefully" in {
    // Setup mocks
    when(userRepository.findByEmail("john@example.com")).thenReturn(Future.successful(None))
    when(userRepository.save(any[User])).thenReturn(Future.successful(User(1, "John Doe", "john@example.com", 25)))
    when(emailService.sendWelcomeEmail(any[User])).thenReturn(Future.successful(false))
    when(auditService.logUserAction(any[Long], any[String], any[Map[String, Any]])).thenReturn(Future.successful(()))

    // Execute
    val result = userService.createUser("John Doe", "john@example.com", 25).futureValue

    // Verify user is still created even if email fails
    result shouldBe a[Right[_, _]]

    // Verify audit log includes email failure
    verify(auditService).logUserAction(eq(1L), eq("USER_CREATED"), argThat { (map: Map[String, Any]) =>
      map("emailSent") == false
    })
  }

  "UserService.updateUser" should "update user information" in {
    // Setup
    val existingUser = User(1, "John Doe", "john@example.com", 25)
    when(userRepository.findById(1L)).thenReturn(Future.successful(Some(existingUser)))
    when(userRepository.save(any[User])).thenReturn(Future.successful(existingUser.copy(name = "John Smith")))
    when(auditService.logUserAction(any[Long], any[String], any[Map[String, Any]])).thenReturn(Future.successful(()))

    // Execute
    val result = userService.updateUser(1L, Some("John Smith"), None).futureValue

    // Verify
    result shouldBe a[Right[_, _]]
    result.right.get.name shouldEqual "John Smith"

    verify(auditService).logUserAction(eq(1L), eq("USER_UPDATED"), any[Map[String, Any]])
  }

  it should "handle non-existent users" in {
    // Setup
    when(userRepository.findById(999L)).thenReturn(Future.successful(None))

    // Execute
    val result = userService.updateUser(999L, Some("New Name"), None).futureValue

    // Verify
    result shouldBe Left("User not found")

    verify(userRepository, never()).save(any[User])
    verify(auditService, never()).logUserAction(any[Long], any[String], any[Map[String, Any]])
  }
}

// Advanced mocking patterns
class AdvancedMockingPatterns extends AnyFlatSpec with Matchers with MockitoSugar {

  implicit val ec: ExecutionContext = ExecutionContext.global

  // Argument matchers and custom verification
  "Advanced argument matching" should "work with complex types" in {
    val userRepository = mock[UserRepository]

    // Custom argument matcher
    def userWithEmail(email: String): User = argThat { (user: User) =>
      user.email == email
    }

    when(userRepository.save(userWithEmail("test@example.com")))
      .thenReturn(Future.successful(User(1, "Test", "test@example.com", 25)))

    val user = User(0, "Test", "test@example.com", 25)
    val result = userRepository.save(user).futureValue

    result.id shouldEqual 1
    verify(userRepository).save(userWithEmail("test@example.com"))
  }

  // Stubbing with side effects
  "Mock with side effects" should "execute additional logic" in {
    val emailService = mock[EmailService]
    var emailsSent = 0

    when(emailService.sendWelcomeEmail(any[User])).thenAnswer { _ =>
      emailsSent += 1
      Future.successful(true)
    }

    val user = User(1, "Test", "test@example.com", 25)
    emailService.sendWelcomeEmail(user).futureValue shouldBe true
    emailsSent shouldEqual 1
  }

  // Verification with timeouts and ordering
  "Verification patterns" should "support timeouts and ordering" in {
    val auditService = mock[AuditService]

    // Setup async operation
    Future {
      Thread.sleep(100)
      auditService.logUserAction(1L, "TEST", Map.empty)
    }

    // Verify with timeout
    verify(auditService, timeout(200)).logUserAction(1L, "TEST", Map.empty)
  }

  // Spy pattern for partial mocking
  "Spy pattern" should "allow partial mocking" in {
    class RealEmailService extends EmailService {
      def sendWelcomeEmail(user: User): Future[Boolean] = {
        // Real implementation
        Future.successful(true)
      }

      def sendPasswordResetEmail(email: String, token: String): Future[Boolean] = {
        // Real implementation
        Future.successful(true)
      }
    }

    val emailService = spy(new RealEmailService)

    // Mock only specific methods
    when(emailService.sendPasswordResetEmail(any[String], any[String]))
      .thenReturn(Future.successful(false))

    // Real method works normally
    emailService.sendWelcomeEmail(User(1, "Test", "test@example.com", 25)).futureValue shouldBe true

    // Mocked method returns mocked value
    emailService.sendPasswordResetEmail("test@example.com", "token").futureValue shouldBe false
  }
}

Integration Testing Strategies

Database Integration Testing

// Database integration testing with embedded database
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
import slick.jdbc.H2Profile.api._
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext}

// Database schema definition
object DatabaseSchema {

  class UsersTable(tag: Tag) extends Table[(Long, String, String, Int, Boolean)](tag, "users") {
    def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
    def name = column[String]("name")
    def email = column[String]("email", O.Unique)
    def age = column[Int]("age")
    def active = column[Boolean]("active", O.Default(true))

    def * = (id, name, email, age, active)
  }

  val users = TableQuery[UsersTable]

  class OrdersTable(tag: Tag) extends Table[(Long, Long, String, BigDecimal)](tag, "orders") {
    def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
    def userId = column[Long]("user_id")
    def status = column[String]("status")
    def total = column[BigDecimal]("total")

    def * = (id, userId, status, total)
    def user = foreignKey("fk_user", userId, users)(_.id)
  }

  val orders = TableQuery[OrdersTable]
}

// Repository implementation
class SlickUserRepository(db: Database)(implicit ec: ExecutionContext) extends UserRepository {

  import DatabaseSchema._

  def findById(id: Long): Future[Option[User]] = {
    val query = users.filter(_.id === id).result.headOption
    db.run(query).map(_.map(tupleToUser))
  }

  def save(user: User): Future[User] = {
    if (user.id == 0) {
      // Insert new user
      val insertQuery = (users.map(u => (u.name, u.email, u.age, u.active))
        returning users.map(_.id)
        into ((userData, id) => tupleToUser((id, userData._1, userData._2, userData._3, userData._4)))
      ) += (user.name, user.email, user.age, user.active)

      db.run(insertQuery)
    } else {
      // Update existing user
      val updateQuery = users.filter(_.id === user.id)
        .map(u => (u.name, u.email, u.age, u.active))
        .update((user.name, user.email, user.age, user.active))

      db.run(updateQuery).map(_ => user)
    }
  }

  def delete(id: Long): Future[Boolean] = {
    val deleteQuery = users.filter(_.id === id).delete
    db.run(deleteQuery).map(_ > 0)
  }

  def findByEmail(email: String): Future[Option[User]] = {
    val query = users.filter(_.email === email).result.headOption
    db.run(query).map(_.map(tupleToUser))
  }

  private def tupleToUser(tuple: (Long, String, String, Int, Boolean)): User = {
    User(tuple._1, tuple._2, tuple._3, tuple._4, tuple._5)
  }
}

// Database integration test
class DatabaseIntegrationTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach {

  implicit val ec: ExecutionContext = ExecutionContext.global

  var database: Database = _
  var userRepository: SlickUserRepository = _

  override def beforeAll(): Unit = {
    // Setup in-memory H2 database
    database = Database.forConfig("h2test", ConfigFactory.parseString(
      """
      h2test {
        driver = "slick.driver.H2Driver$"
        db {
          driver = "org.h2.Driver"
          url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
          user = "sa"
          password = ""
        }
      }
      """
    ))

    userRepository = new SlickUserRepository(database)

    // Create schema
    val setupFuture = database.run(DBIO.seq(
      DatabaseSchema.users.schema.create,
      DatabaseSchema.orders.schema.create
    ))

    Await.result(setupFuture, 10.seconds)
  }

  override def afterAll(): Unit = {
    if (database != null) {
      database.close()
    }
  }

  override def beforeEach(): Unit = {
    // Clean data before each test
    val cleanupFuture = database.run(DBIO.seq(
      DatabaseSchema.orders.delete,
      DatabaseSchema.users.delete
    ))

    Await.result(cleanupFuture, 5.seconds)
  }

  "UserRepository" should "save and retrieve users" in {
    val user = User(0, "John Doe", "john@example.com", 30)

    val savedUser = userRepository.save(user).futureValue
    savedUser.id should be > 0L
    savedUser.name shouldEqual "John Doe"

    val retrievedUser = userRepository.findById(savedUser.id).futureValue
    retrievedUser shouldBe Some(savedUser)
  }

  it should "find users by email" in {
    val user = User(0, "Jane Doe", "jane@example.com", 28)
    val savedUser = userRepository.save(user).futureValue

    val foundUser = userRepository.findByEmail("jane@example.com").futureValue
    foundUser shouldBe Some(savedUser)

    val notFound = userRepository.findByEmail("nonexistent@example.com").futureValue
    notFound shouldBe None
  }

  it should "update existing users" in {
    val user = User(0, "Bob Smith", "bob@example.com", 35)
    val savedUser = userRepository.save(user).futureValue

    val updatedUser = savedUser.copy(name = "Robert Smith", age = 36)
    val result = userRepository.save(updatedUser).futureValue

    result.name shouldEqual "Robert Smith"
    result.age shouldEqual 36

    val retrieved = userRepository.findById(savedUser.id).futureValue
    retrieved.get.name shouldEqual "Robert Smith"
  }

  it should "delete users" in {
    val user = User(0, "Delete Me", "delete@example.com", 25)
    val savedUser = userRepository.save(user).futureValue

    val deleted = userRepository.delete(savedUser.id).futureValue
    deleted shouldBe true

    val notFound = userRepository.findById(savedUser.id).futureValue
    notFound shouldBe None
  }

  it should "handle unique constraint violations" in {
    val user1 = User(0, "User One", "same@example.com", 25)
    val user2 = User(0, "User Two", "same@example.com", 30)

    userRepository.save(user1).futureValue

    an[Exception] should be thrownBy {
      userRepository.save(user2).futureValue
    }
  }
}

// Transaction testing
class TransactionIntegrationTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach {

  implicit val ec: ExecutionContext = ExecutionContext.global

  var database: Database = _

  override def beforeAll(): Unit = {
    database = Database.forConfig("h2test")
    // Setup schema...
  }

  override def afterAll(): Unit = {
    database.close()
  }

  "Transaction handling" should "rollback on failure" in {
    import DatabaseSchema._

    val transactionAction = DBIO.seq(
      users += (0L, "User 1", "user1@example.com", 25, true),
      users += (0L, "User 2", "user2@example.com", 30, true),
      // This should cause a failure due to duplicate email
      users += (0L, "User 3", "user1@example.com", 35, true)
    ).transactionally

    an[Exception] should be thrownBy {
      Await.result(database.run(transactionAction), 5.seconds)
    }

    // Verify rollback - no users should be inserted
    val count = Await.result(database.run(users.length.result), 5.seconds)
    count shouldEqual 0
  }

  it should "commit on success" in {
    import DatabaseSchema._

    val transactionAction = DBIO.seq(
      users += (0L, "User 1", "user1@example.com", 25, true),
      users += (0L, "User 2", "user2@example.com", 30, true),
      users += (0L, "User 3", "user3@example.com", 35, true)
    ).transactionally

    Await.result(database.run(transactionAction), 5.seconds)

    // Verify commit - all users should be inserted
    val count = Await.result(database.run(users.length.result), 5.seconds)
    count shouldEqual 3
  }
}

HTTP API Integration Testing

// HTTP API integration testing
import akka.actor.testkit.typed.scaladsl.ActorTestKit
import akka.actor.typed.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.http.scaladsl.unmarshalling.Unmarshal
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import spray.json._
import DefaultJsonProtocol._
import scala.concurrent.duration._

// JSON protocols
object JsonProtocols {
  implicit val userFormat: RootJsonFormat[User] = jsonFormat5(User)

  case class CreateUserRequest(name: String, email: String, age: Int)
  implicit val createUserRequestFormat: RootJsonFormat[CreateUserRequest] = jsonFormat3(CreateUserRequest)

  case class UpdateUserRequest(name: Option[String], email: Option[String])
  implicit val updateUserRequestFormat: RootJsonFormat[UpdateUserRequest] = jsonFormat2(UpdateUserRequest)

  case class ErrorResponse(error: String, code: String)
  implicit val errorResponseFormat: RootJsonFormat[ErrorResponse] = jsonFormat2(ErrorResponse)
}

// HTTP routes
class UserRoutes(userService: UserService)(implicit system: ActorSystem[_]) {

  import akka.http.scaladsl.server.Directives._
  import JsonProtocols._
  import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._

  implicit val ec = system.executionContext

  val routes = 
    pathPrefix("api" / "v1" / "users") {
      concat(
        // POST /api/v1/users
        pathEnd {
          post {
            entity(as[CreateUserRequest]) { request =>
              onComplete(userService.createUser(request.name, request.email, request.age)) {
                case Success(Right(user)) =>
                  complete(StatusCodes.Created, user)
                case Success(Left(error)) =>
                  complete(StatusCodes.BadRequest, ErrorResponse(error, "VALIDATION_ERROR"))
                case Failure(exception) =>
                  complete(StatusCodes.InternalServerError, ErrorResponse("Internal server error", "INTERNAL_ERROR"))
              }
            }
          }
        },
        // GET /api/v1/users/{id}
        path(LongNumber) { id =>
          get {
            onComplete(userService.findById(id)) {
              case Success(Some(user)) =>
                complete(StatusCodes.OK, user)
              case Success(None) =>
                complete(StatusCodes.NotFound, ErrorResponse("User not found", "NOT_FOUND"))
              case Failure(exception) =>
                complete(StatusCodes.InternalServerError, ErrorResponse("Internal server error", "INTERNAL_ERROR"))
            }
          }
        },
        // PUT /api/v1/users/{id}
        path(LongNumber) { id =>
          put {
            entity(as[UpdateUserRequest]) { request =>
              onComplete(userService.updateUser(id, request.name, request.email)) {
                case Success(Right(user)) =>
                  complete(StatusCodes.OK, user)
                case Success(Left(error)) =>
                  complete(StatusCodes.BadRequest, ErrorResponse(error, "VALIDATION_ERROR"))
                case Failure(exception) =>
                  complete(StatusCodes.InternalServerError, ErrorResponse("Internal server error", "INTERNAL_ERROR"))
              }
            }
          }
        }
      )
    }
}

// Route testing
class UserRoutesSpec extends AnyFlatSpec with Matchers with ScalatestRouteTest with MockitoSugar with ScalaFutures {

  import JsonProtocols._
  import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._

  implicit val timeout: RouteTestTimeout = RouteTestTimeout(5.seconds)

  val mockUserService = mock[UserService]
  val userRoutes = new UserRoutes(mockUserService)

  "POST /api/v1/users" should "create a new user" in {
    val request = CreateUserRequest("John Doe", "john@example.com", 30)
    val expectedUser = User(1, "John Doe", "john@example.com", 30)

    when(mockUserService.createUser("John Doe", "john@example.com", 30))
      .thenReturn(Future.successful(Right(expectedUser)))

    Post("/api/v1/users", request) ~> userRoutes.routes ~> check {
      status shouldEqual StatusCodes.Created
      responseAs[User] shouldEqual expectedUser
    }
  }

  it should "return validation error for invalid data" in {
    val request = CreateUserRequest("John Doe", "john@example.com", 17)

    when(mockUserService.createUser("John Doe", "john@example.com", 17))
      .thenReturn(Future.successful(Left("User must be at least 18 years old")))

    Post("/api/v1/users", request) ~> userRoutes.routes ~> check {
      status shouldEqual StatusCodes.BadRequest
      responseAs[ErrorResponse].error shouldEqual "User must be at least 18 years old"
    }
  }

  "GET /api/v1/users/{id}" should "return user when found" in {
    val user = User(1, "John Doe", "john@example.com", 30)

    when(mockUserService.findById(1L))
      .thenReturn(Future.successful(Some(user)))

    Get("/api/v1/users/1") ~> userRoutes.routes ~> check {
      status shouldEqual StatusCodes.OK
      responseAs[User] shouldEqual user
    }
  }

  it should "return 404 when user not found" in {
    when(mockUserService.findById(999L))
      .thenReturn(Future.successful(None))

    Get("/api/v1/users/999") ~> userRoutes.routes ~> check {
      status shouldEqual StatusCodes.NotFound
      responseAs[ErrorResponse].code shouldEqual "NOT_FOUND"
    }
  }

  "PUT /api/v1/users/{id}" should "update user successfully" in {
    val request = UpdateUserRequest(Some("Jane Doe"), None)
    val updatedUser = User(1, "Jane Doe", "john@example.com", 30)

    when(mockUserService.updateUser(1L, Some("Jane Doe"), None))
      .thenReturn(Future.successful(Right(updatedUser)))

    Put("/api/v1/users/1", request) ~> userRoutes.routes ~> check {
      status shouldEqual StatusCodes.OK
      responseAs[User] shouldEqual updatedUser
    }
  }
}

// End-to-end API testing
class UserAPIIntegrationTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll with ScalaFutures {

  import JsonProtocols._
  import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._

  implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "test-system")
  implicit val ec = system.executionContext

  var serverBinding: Http.ServerBinding = _
  var database: Database = _

  override def beforeAll(): Unit = {
    // Setup test database
    database = Database.forConfig("h2test")
    val userRepository = new SlickUserRepository(database)
    val mockEmailService = mock[EmailService]
    val mockAuditService = mock[AuditService]

    // Setup mocks
    when(mockEmailService.sendWelcomeEmail(any[User])).thenReturn(Future.successful(true))
    when(mockAuditService.logUserAction(any[Long], any[String], any[Map[String, Any]])).thenReturn(Future.successful(()))

    val userService = new UserService(userRepository, mockEmailService, mockAuditService)
    val userRoutes = new UserRoutes(userService)

    // Start server
    serverBinding = Await.result(
      Http().newServerAt("localhost", 0).bind(userRoutes.routes),
      10.seconds
    )
  }

  override def afterAll(): Unit = {
    if (serverBinding != null) {
      Await.result(serverBinding.unbind(), 5.seconds)
    }
    if (database != null) {
      database.close()
    }
    Await.result(system.terminate(), 5.seconds)
  }

  "User API" should "handle complete user lifecycle" in {
    val baseUrl = s"http://localhost:${serverBinding.localAddress.getPort}"

    // Create user
    val createRequest = HttpRequest(
      method = HttpMethods.POST,
      uri = s"$baseUrl/api/v1/users",
      entity = HttpEntity(
        ContentTypes.`application/json`,
        CreateUserRequest("Integration Test", "integration@example.com", 25).toJson.toString
      )
    )

    val createResponse = Http().singleRequest(createRequest).futureValue
    createResponse.status shouldEqual StatusCodes.Created

    val createdUser = Unmarshal(createResponse.entity).to[User].futureValue
    createdUser.name shouldEqual "Integration Test"
    createdUser.id should be > 0L

    // Get user
    val getRequest = HttpRequest(
      method = HttpMethods.GET,
      uri = s"$baseUrl/api/v1/users/${createdUser.id}"
    )

    val getResponse = Http().singleRequest(getRequest).futureValue
    getResponse.status shouldEqual StatusCodes.OK

    val retrievedUser = Unmarshal(getResponse.entity).to[User].futureValue
    retrievedUser shouldEqual createdUser

    // Update user
    val updateRequest = HttpRequest(
      method = HttpMethods.PUT,
      uri = s"$baseUrl/api/v1/users/${createdUser.id}",
      entity = HttpEntity(
        ContentTypes.`application/json`,
        UpdateUserRequest(Some("Updated Name"), None).toJson.toString
      )
    )

    val updateResponse = Http().singleRequest(updateRequest).futureValue
    updateResponse.status shouldEqual StatusCodes.OK

    val updatedUser = Unmarshal(updateResponse.entity).to[User].futureValue
    updatedUser.name shouldEqual "Updated Name"
    updatedUser.email shouldEqual createdUser.email
  }
}

Test Automation and CI Integration

Comprehensive Test Suite Organization

// Test categories and organization
import org.scalatest.Tag

object TestTags {
  object UnitTest extends Tag("UnitTest")
  object IntegrationTest extends Tag("IntegrationTest")
  object PerformanceTest extends Tag("PerformanceTest")
  object SlowTest extends Tag("SlowTest")
  object DatabaseTest extends Tag("DatabaseTest")
  object ExternalServiceTest extends Tag("ExternalServiceTest")
}

// Base test traits
trait UnitTestBase extends AnyFlatSpec with Matchers with MockitoSugar {
  // Common setup for unit tests
}

trait IntegrationTestBase extends AnyFlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach {
  // Common setup for integration tests
}

trait DatabaseTestBase extends IntegrationTestBase {
  // Database-specific setup
  var database: Database = _

  override def beforeAll(): Unit = {
    super.beforeAll()
    // Setup test database
  }

  override def afterAll(): Unit = {
    if (database != null) {
      database.close()
    }
    super.afterAll()
  }
}

// Test data builders
object TestDataBuilders {

  def userBuilder(
    id: Long = 0,
    name: String = "Test User",
    email: String = "test@example.com",
    age: Int = 25,
    active: Boolean = true
  ): User = User(id, name, email, age, active)

  def createUserRequestBuilder(
    name: String = "Test User",
    email: String = "test@example.com",
    age: Int = 25
  ): CreateUserRequest = CreateUserRequest(name, email, age)

  // Builder pattern for complex objects
  case class UserBuilder(
    id: Long = 0,
    name: String = "Test User",
    email: String = "test@example.com",
    age: Int = 25,
    active: Boolean = true
  ) {
    def withId(id: Long): UserBuilder = copy(id = id)
    def withName(name: String): UserBuilder = copy(name = name)
    def withEmail(email: String): UserBuilder = copy(email = email)
    def withAge(age: Int): UserBuilder = copy(age = age)
    def inactive: UserBuilder = copy(active = false)
    def build: User = User(id, name, email, age, active)
  }

  // Factory methods for common scenarios
  object Users {
    def validUser: User = UserBuilder().build
    def userWithId(id: Long): User = UserBuilder().withId(id).build
    def minorUser: User = UserBuilder().withAge(17).build
    def inactiveUser: User = UserBuilder().inactive.build
    def userWithEmail(email: String): User = UserBuilder().withEmail(email).build
  }
}

// Performance testing framework
class PerformanceTestSuite extends AnyFlatSpec with Matchers with ScalaFutures {

  import TestTags._
  import scala.concurrent.duration._

  "UserService performance" should "handle high load" taggedAs(PerformanceTest, SlowTest) in {
    val userService = createUserService() // Implementation-specific

    val startTime = System.nanoTime()
    val futures = (1 to 1000).map { i =>
      userService.createUser(s"User $i", s"user$i@example.com", 25)
    }

    val results = Future.sequence(futures).futureValue(timeout(30.seconds))
    val endTime = System.nanoTime()

    val duration = (endTime - startTime).nanos
    val throughput = results.size.toDouble / duration.toSeconds

    results should have size 1000
    results.count(_.isRight) should be >= 900 // Allow some failures
    throughput should be > 100.0 // requests per second

    println(s"Performance: ${results.size} operations in ${duration.toMillis}ms, throughput: $throughput ops/sec")
  }

  private def createUserService(): UserService = {
    // Create service with real or mock dependencies
    // This would be implementation-specific
    ???
  }
}

// Test report generation
object TestReporting {

  case class TestResult(
    testName: String,
    className: String,
    status: String,
    duration: Duration,
    error: Option[String] = None
  )

  case class TestSummary(
    totalTests: Int,
    passed: Int,
    failed: Int,
    skipped: Int,
    duration: Duration,
    coverage: Double
  )

  def generateHTMLReport(results: List[TestResult], summary: TestSummary): String = {
    s"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>Test Report</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            .summary { background: #f5f5f5; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
            .passed { color: green; }
            .failed { color: red; }
            .skipped { color: orange; }
            table { border-collapse: collapse; width: 100%; }
            th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
            th { background-color: #f2f2f2; }
        </style>
    </head>
    <body>
        <h1>Test Report</h1>
        <div class="summary">
            <h2>Summary</h2>
            <p>Total Tests: ${summary.totalTests}</p>
            <p class="passed">Passed: ${summary.passed}</p>
            <p class="failed">Failed: ${summary.failed}</p>
            <p class="skipped">Skipped: ${summary.skipped}</p>
            <p>Duration: ${summary.duration.toSeconds}s</p>
            <p>Coverage: ${summary.coverage}%</p>
        </div>

        <h2>Test Results</h2>
        <table>
            <tr>
                <th>Test Name</th>
                <th>Class</th>
                <th>Status</th>
                <th>Duration</th>
                <th>Error</th>
            </tr>
            ${results.map { result =>
              s"""
              <tr>
                  <td>${result.testName}</td>
                  <td>${result.className}</td>
                  <td class="${result.status.toLowerCase}">${result.status}</td>
                  <td>${result.duration.toMillis}ms</td>
                  <td>${result.error.getOrElse("")}</td>
              </tr>
              """
            }.mkString}
        </table>
    </body>
    </html>
    """
  }
}

Conclusion

Advanced testing in Scala encompasses multiple strategies and techniques that work together to ensure application reliability:

Property-Based Testing Benefits:

  • Discovers edge cases through random input generation
  • Tests invariants and mathematical properties
  • Provides confidence in algorithm correctness
  • Scales testing beyond manual test case creation

Mocking and Test Doubles:

  • Isolates units under test from dependencies
  • Enables fast, reliable unit testing
  • Supports complex interaction verification
  • Facilitates testing of error conditions

Integration Testing Strategies:

  • Validates component interactions
  • Tests database integration and transactions
  • Verifies HTTP API behavior end-to-end
  • Ensures system functionality across boundaries

Test Automation Framework:

  • Organizes tests by type and execution requirements
  • Provides consistent test data creation
  • Enables performance and load testing
  • Generates comprehensive test reports

Best Practices:

  • Use appropriate testing strategy for each component level
  • Balance between unit, integration, and end-to-end tests
  • Maintain test independence and reliability
  • Focus on meaningful assertions and clear test intent
  • Automate test execution in CI/CD pipelines

Testing Pyramid Structure:

  • Many fast, isolated unit tests
  • Moderate number of integration tests
  • Few but comprehensive end-to-end tests
  • Performance tests for critical paths

Quality Metrics:

  • Code coverage measurement and targets
  • Test execution time and reliability
  • Defect detection effectiveness
  • Regression prevention capability

Continuous Improvement:

  • Regular review of test effectiveness
  • Refactoring of test code for maintainability
  • Investment in test infrastructure and tooling
  • Team education on testing best practices

Modern Scala testing leverages the language's strengths while addressing the complexities of distributed systems, concurrent programming, and integration challenges. The goal is building confidence in system behavior through comprehensive, maintainable, and efficient test suites that catch issues early and prevent regressions.