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.

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.
ScalaCheck adalah library property-based untuk Scala. Alih-alih menulis 20 contoh, kalian menulis property (pernyataan universal) dan ScalaCheck mencari counter-example:
libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.18.1" % Testimport 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:
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.
ScalaCheck terintegrasi dengan MUnit via trait ScalaCheckSuite:
libraryDependencies += "org.scalameta" %% "munit-scalacheck" % "1.0.0" % Testimport 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
}
}Pola law-based merangkum properti khas sebuah tipe. Contoh: hukum Semigroup adalah associativity — (a op b) op c == a op (b op c):
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.
Terkadang kode bergantung pada objek eksternal (klien HTTP, repository) yang sulit dibuat di test. Mock menggantikan objek tersebut dengan tiruan yang perilakunya dikendalikan:
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 adalah mocking library berbasis reflection untuk Scala, tanpa Java interop:
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 menjalankan dependency nyata (PostgreSQL, Redis, Kafka) di dalam Docker hanya selama test — deterministik dan bersih. testcontainers-scala adalah wrapper Scala-nya:
libraryDependencies ++= Seq(
"com.dimafeng" %% "testcontainers-scala-scalatest" % "0.41.5" % Test,
"com.dimafeng" %% "testcontainers-scala-postgresql" % "0.41.5" % Test
)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.
a * b == b * a gagal karena overflow Int; kenali batas generator dan gunakan tipe yang sesuai (mis. Long).Gen.choose, Gen.nonEmptyListOf) bila perlu mengontrol distribusi input.Inti yang harus dibawa pulang:
forAll menguji properti terhadap ratusan input acak — menemukan kasus yang tidak terpikirkan.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!