Belajar Scala - Testing Lanjut & Property-based
Episode 19 of 23

Belajar Scala - Testing Lanjut & Property-based

Menghabiskan strategi testing dengan property-based testing lewat ScalaCheck forAll dan law-based testing, lalu mocking dengan Mockito dan mock4s, serta integration testing sungguhan memakai testcontainers-scala untuk database dan Redis.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Di episode 10 kita menulis test dengan assertion tetap — contoh, contoh, dan contoh. Pendekatan itu punya kelemahan: kalian hanya menguji input yang terpikirkan. Bagaimana dengan input yang tidak terpikirkan — yang justru sering jadi bug di produksi? Jawabannya adalah property-based testing, topik utama episode ini.

Mengapa penting? Karena property-based testing membalik pendekatan: kalian mendefinisikan hukum/aturan yang harus selalu benar, lalu generator membuat ratusan input otomatis. Ditambah mocking dan testcontainers, episode ini melengkapi perlengkapan testing kalian untuk produksi.

Property-based Testing dengan ScalaCheck

ScalaCheck adalah library property-based untuk Scala. Alih-alih menulis 20 contoh, kalian menulis property (pernyataan universal) dan ScalaCheck mencari counter-example:

Dependency ScalaCheck
libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.18.1" % Test
Property pertama dengan forAll
import org.scalacheck.Prop.forAll
import org.scalacheck.Properties
 
object ListProps extends Properties("List"):
  property("reverse dua kali = identitas") = forAll { (xs: List[Int]) =>
    xs.reverse.reverse == xs
  }
 
  property("head dari kons = elemen pertama") = forAll { (xs: List[Int]) =>
    xs.nonEmpty ==> (xs.head :: xs.tail == xs)
  }

forAll { (xs: List[Int]) => ... } menjalankan properti terhadap ratusan list acak — kosong, besar, negatif, duplikat. Jika ada yang melanggar, ScalaCheck melaporkan counter-example:

Contoh kegagalan property
Falsified after 12 successful tests:
  Arg0 = List(2147483647, -2147483648)

Kekuatannya: perilaku yang jarang terjadi ditemukan otomatis — misal overflow, list kosong, atau nilai ekstrem — yang hampir mustahil kalian tulis manual.

Integrasi dengan MUnit

ScalaCheck terintegrasi dengan MUnit via trait ScalaCheckSuite:

Dependency munit-scalacheck
libraryDependencies += "org.scalameta" %% "munit-scalacheck" % "1.0.0" % Test
Property di MUnit
import munit.ScalaCheckSuite
import org.scalacheck.Prop.*
 
class CalculatorProps extends ScalaCheckSuite:
 
  property("perkalian komutatif") {
    forAll { (a: Int, b: Int) =>
      (a * b) == (b * a)
    }
  }
 
  property("penjumlahan mengembalikan Right untuk input valid") {
    forAll { (a: Int, b: Int) =>
      Calculator.divide(a, b) match
        case Left(_)  => b == 0
        case Right(_) => b != 0
    }
  }

Law-based Testing

Pola law-based merangkum properti khas sebuah tipe. Contoh: hukum Semigroup adalah associativity(a op b) op c == a op (b op c):

Law Semigroup
import org.scalacheck.Prop.forAll
import org.scalacheck.Properties
 
object StringSemigroupProps extends Properties("String concat"):
  property("associatif") = forAll { (a: String, b: String, c: String) =>
    (a + b) + c == a + (b + c)
  }

Library FP (cats, ZIO) menyediakan kumpulan law untuk typeclass mereka. Menguji law bukan sekadar formalitas: hukum menjamin properti komposisi yang membuat kode kalian bisa diprediksi — jika string concat tidak asosiatif, semua pipeline yang mengandalkannya berpotensi salah.

Tip

Kombinasi ideal: unit test untuk contoh spesifik yang sudah diketahui, property test untuk hukum umum dan kasus acak. Keduanya saling melengkapi — unit test mendokumentasikan perilaku, property test menemukan yang belum terpikirkan.

Mocking: Mockito dan mock4s

Terkadang kode bergantung pada objek eksternal (klien HTTP, repository) yang sulit dibuat di test. Mock menggantikan objek tersebut dengan tiruan yang perilakunya dikendalikan:

Mockito (melalui scala-mock interop)

Mockito dengan MockitoScalaSession
import org.mockito.MockitoSugar
 
class UserServiceSpec extends munit.FunSuite with MockitoSugar:
 
  val repo = mock[UserRepository]
 
  test("service memanggil repo dengan benar") {
    when(repo.findById(1)).thenReturn(Some(User(1, "Sari")))
    val service = UserService(repo)
    assertEquals(service.loadName(1), "Sari")
    verify(repo, times(1)).findById(1)
  }

mock4s (library Scala murni)

mock4s adalah mocking library berbasis reflection untuk Scala, tanpa Java interop:

mock4s
import com.fortysevendeg.mocks.scalatest.MockFactory
 
class Spec extends munit.FunSuite with MockFactory:
  val repo = mock[UserRepository]
 
  test("mock4s membuat stub") {
    (() => repo.findById(1)).stub(Some(User(1, "Sari")))
    ...
  }

Aturan yang sehat: jangan over-mock. Jika interaksi dengan dependency semakin kompleks, pertimbangkan fake yang sederhana atau testcontainers untuk integrasi nyata (di bawah). Mock paling berguna untuk unit test cepat yang mengisolasi logika.

Testcontainers: Integrasi Nyata di Container

Testcontainers menjalankan dependency nyata (PostgreSQL, Redis, Kafka) di dalam Docker hanya selama test — deterministik dan bersih. testcontainers-scala adalah wrapper Scala-nya:

Dependency testcontainers-scala
libraryDependencies ++= Seq(
  "com.dimafeng" %% "testcontainers-scala-scalatest" % "0.41.5" % Test,
  "com.dimafeng" %% "testcontainers-scala-postgresql" % "0.41.5" % Test
)
Test dengan PostgreSQL container
import com.dimafeng.testcontainers.PostgreSQLContainer
import org.testcontainers.utility.DockerImageName
import com.dimafeng.testcontainers.scalatest.TestContainerForEach
 
class UserRepositoryIT extends munit.FunSuite with TestContainerForEach:
 
  override val containerDef = PostgreSQLContainer.Def(
    DockerImageName.parse("postgres:16")
  )
 
  test("menyimpan dan membaca user dari postgres nyata") {
    withContainer { postgres =>
      val repo = new UserRepository(postgres.jdbcUrl, postgres.username, postgres.password)
      val saved = repo.save(User(0, "Sari"))
      assertEquals(repo.findById(saved.id).map(_.name), Some("Sari"))
    }
  }

Keunggulan testcontainers: tidak ada kesenjangan antara test dan produksi — query SQL yang sama dijalankan di PostgreSQL asli. Untuk integration test, ini adalah standar industri 2026.

Common Pitfalls

  • Property yang diam-diam melintasi boundarya * b == b * a gagal karena overflow Int; kenali batas generator dan gunakan tipe yang sesuai (mis. Long).
  • Mock yang terlalu banyak — test menjadi fragile dan "menguji implementasi", bukan perilaku. Prioritaskan test nyata.
  • Testcontainers lambat? — wajar; jangan jadikan unit test. Pisahkan ke suite integration test yang berjalan terpisah.
  • Generator ScalaCheck yang biased — gunakan generator khusus (Gen.choose, Gen.nonEmptyListOf) bila perlu mengontrol distribusi input.

Penutup

Inti yang harus dibawa pulang:

  • ScalaCheck forAll menguji properti terhadap ratusan input acak — menemukan kasus yang tidak terpikirkan.
  • Law-based testing menguji hukum tipe (mis. associativity) yang menjamin komposisi aman.
  • Mockito/mock4s mengisolasi unit test; jangan over-mock.
  • testcontainers-scala menjalankan PostgreSQL/Redis/Kafka nyata di container — integration test tanpa kesenjangan produksi.
  • Kombinasi unit + property + integration = strategi testing lengkap.

Di episode 20 selanjutnya, kita melihat ke depan: Scala 3.8 dan fitur terbaru — Better Fors, runtimeChecked, REPL terpisah, dan jalur LTS. Sampai jumpa di episode 20!

Belajar Scala - Testing Lanjut & Property-based | Belajar Scala