Elixir untuk Technical Leader — Bab 7 dari 10

Testing & Quality

Bug yang ketemu di test = gratis. Bug yang ketemu user = mahal. LabaBersih v1 punya 0 test. v2 punya 337+.


Kenapa Test Itu Penting

Coba bayangkan ini: kamu punya function ship_order yang harus melakukan 5 hal sekaligus — ubah status, potong stok, generate jurnal penjualan, generate jurnal HPP, dan catat audit trail. Kalau salah satu gagal, semuanya harus rollback.

Di v1, kamu cuma bisa tau ini jalan dengan benar kalau Nelly coba manual di production. Bug ditemukan oleh user, bukan oleh developer.

v1 — 0 Test

Bug ditemukan oleh Nelly di production.
"Hafish, stoknya gak berkurang setelah kirim order."
Fix, deploy, jangan-jangan ada side effect lain yang rusak.
Siklus: code → deploy → doa → bug report
v2 — 337+ Test

Bug ditemukan mesin dalam 5 detik.
"1 failure: stok tidak berkurang setelah ship."
Fix, run test lagi, semua pass, deploy aman.
Siklus: code → test → fix → deploy → tidur nyenyak
Analogi: Asuransi Test itu seperti asuransi. Kamu bayar di depan (waktu nulis test), supaya gak bayar lebih mahal belakangan (bug di production). Bedanya, asuransi ini gak pernah gagal klaim.

Rule LabaBersih #5: "Setiap function WAJIB punya test." Gak ada pengecualian.


ExUnit — Framework Test Bawaan

Elixir punya framework test built-in bernama ExUnit. Gak perlu install library tambahan, gak perlu config apapun. Langsung jalan.

JavaScript (Jest)

npm install --save-dev jest
Edit package.json
Tambah jest.config.js
Kadang error: "Cannot find module"
Elixir (ExUnit)

Sudah ada.
Gak perlu install.
Gak perlu config.
mix test — selesai.

Jalankan semua test:

$ mix test
# Output: "337 tests, 0 failures" ← ini yang kamu mau lihat

Struktur Test — 3 Bagian

Setiap test punya 3 bagian: describe (kelompok), test (skenario), dan assert (verifikasi).

describe "ship_order/2" do
  test "ship order berhasil → status + stok + jurnal" do
    # 1. ARRANGE — siapkan data
    ctx = setup_full!()
    order = create_order!(ctx)

    # 2. ACT — jalankan function yang di-test
    {:ok, result} = Orders.ship_order(order.id, "nelly@bestari.id")

    # 3. ASSERT — verifikasi hasilnya
    assert result.order.fulfillment_status == "shipped"
    assert result.order.shipped_at != nil

    product = Inventory.get_product!(ctx.product.id)
    assert product.stok == 98
  end
end
Cara baca test ini
  1. describe "ship_order/2" — kita lagi nge-test function ship_order yang terima 2 argumen
  2. test "ship order berhasil..." — skenario: kirim order yang valid, harusnya berhasil
  3. assert result.order.fulfillment_status == "shipped" — cek: statusnya HARUS jadi "shipped"
  4. assert product.stok == 98 — cek: stok HARUS turun dari 100 ke 98 (kirim 2 item)
Kalau salah satu assert gagal, test GAGAL. Berarti ada bug.

Konvensi File Test

File test mirror file source code. Kalau kamu tau ada file lib/lababersih/orders/orders.ex, kamu otomatis tau test-nya di test/lababersih/orders/orders_test.exs.

Source CodeFile Test
lib/lababersih/orders/orders.extest/lababersih/orders/orders_test.exs
lib/lababersih/accounting/accounting.extest/lababersih/accounting/accounting_test.exs
lib/lababersih/inventory/inventory.extest/lababersih/inventory/inventory_test.exs
lib/lababersih/returns/returns.extest/lababersih/returns/returns_test.exs
lib/lababersih/customers/customers.extest/lababersih/customers/customers_test.exs

Polanya selalu: lib/test/, dan nama file ditambah _test.


Setup — Data Segar Setiap Test

Setiap test butuh data untuk di-test. Di LabaBersih, setup_full!() bikin data lengkap: user, organisasi, toko, produk, stok.

defp setup_full!() do
  # Buat user + organisasi
  {:ok, %{user: user, organization: org}} =
    Accounts.register_user(%{
      email: "test-#{System.unique_integer([:positive])}@example.com",
      name: "Test User",
      password: "password123"
    })

  # Buat akun akuntansi minimal
  # Buat toko + fee mapping
  # Buat produk + stok awal 100
  # Return semua data
  %{org: org, user: user, store: store, product: product}
end
Database Sandbox Setiap test jalan di transaksi database terpisah yang di-rollback setelah test selesai. Artinya: test A bikin order, test B gak akan lihat order itu. Setiap test dapet database bersih — gak ada sisa dari test sebelumnya. Ini otomatis, kamu gak perlu bersihkan manual.

Kenapa ini penting? Karena test yang saling mengganggu = nightmare. "Test A pass sendiri tapi gagal kalau bareng test B" — itu gak pernah terjadi dengan sandbox.


Assertions — Cara Verifikasi

Assertion = klaim yang HARUS benar. Kalau salah, test gagal. Ada beberapa jenis:

assert — harus benar

# Cek status order jadi "shipped"
assert order.fulfillment_status == "shipped"

# Cek stok berkurang
assert product.stok == 98

# Cek shipped_at terisi (bukan nil)
assert order.shipped_at != nil

refute — harus salah (kebalikan assert)

# Cek order BUKAN status "dibuat"
refute order.order_status == "dibuat"

# Cek akun TIDAK aktif
refute account.is_active

Pattern matching dalam assert

# Cek return value cocok pattern {:ok, ...}
assert {:ok, result} = Orders.ship_order(order.id, "nelly@bestari.id")

# Cek error spesifik
assert {:error, :already_shipped} = Orders.ship_order(order.id, "nelly@bestari.id")

# Cek error dengan detail
assert {:error, {:insufficient_available_stock, _, _, _}} =
  Orders.create_order(big_order_attrs, items)

assert_raise — harus error (exception)

# Cek kalau order gak ada → raise error
assert_raise Ecto.NoResultsError, fn ->
  Orders.get_order!("order-yang-gak-ada")
end
AssertionArtinyaKapan pakai
assert X == YX harus sama dengan YPaling sering — cek hasil
assert XX harus truthy (bukan nil/false)Cek field terisi
refute XX harus falsyCek field TIDAK ada / false
assert {:ok, _} = func()Return harus match patternCek function berhasil
assert {:error, reason} = func()Return harus errorCek function gagal dengan benar
assert_raise Error, fnHarus raise exceptionCek data tidak ditemukan

Pola Testing di LabaBersih

Setiap function penting punya minimal 3 test: happy path, error path, dan edge case. Ini bukan teori — ini dari kode LabaBersih yang asli.

1. Happy Path — semuanya berjalan benar

test "ship order → atomic: status + stok + jurnal + audit" do
  ctx = setup_full!()
  order = create_order!(ctx)

  {:ok, result} = Orders.ship_order(order.id, "nelly@bestari.id")

  # Status berubah
  assert result.order.fulfillment_status == "shipped"
  assert result.order.shipped_at != nil

  # Stok berkurang (2 pcs dari 100)
  product = Inventory.get_product!(ctx.product.id)
  assert product.stok == 98

  # HPP dari FIFO lot (2 x Rp50.000 = Rp100.000)
  assert Decimal.eq?(result.hpp, Decimal.new(100_000))

  # Jurnal tercipta (penjualan + HPP)
  assert length(result.journal_ids) == 2
end
Kenapa test ini penting Test ini memverifikasi 5 side effect sekaligus dari 1 function call. Kalau salah satu gagal (misal: stok gak berkurang), kamu langsung tau. Tanpa test, bug ini bisa terlewat berminggu-minggu.

2. Error Path — input yang salah harus ditolak

test "create order stok gak cukup → rollback" do
  ctx = setup_full!()

  # Coba buat order 999 pcs (stok cuma 100)
  assert {:error, {:insufficient_available_stock, _, _, _}} =
    Orders.create_order(
      %{org_id: ctx.org.id, platform: "tiktok", store_id: ctx.store.id,
        total_harga: 5_000_000, customer_name: "Big Buyer"},
      [%{product_id: ctx.product.id, product_name: "Forbest",
         sku: "FB-001", quantity: 999, price: 5_000}]
    )

  # Stok TETAP 100 (rollback)
  product = Inventory.get_product!(ctx.product.id)
  assert product.stok == 100
  assert product.reserved_stock == 0
end
Perhatikan Test ini bukan cuma cek "ada error". Tapi juga cek setelah error, data gak rusak. Stok tetap 100, reserved tetap 0. Ini yang namanya test atomicity — kalau gagal, SEMUANYA harus rollback.

3. Edge Case — idempotent (aman dipanggil 2x)

test "ship order yang sudah dikirim → idempotent skip" do
  ctx = setup_full!()
  order = create_order!(ctx)

  # Ship pertama → berhasil
  {:ok, _} = Orders.ship_order(order.id, "nelly@bestari.id")

  # Ship kedua → skip, bukan error crash
  assert {:error, :already_shipped} = Orders.ship_order(order.id, "nelly@bestari.id")
end
Kenapa idempotent penting di LabaBersih XLSX import dan API sync bisa jalan bersamaan. Bagaimana kalau keduanya coba ship order yang sama? Tanpa idempotent, stok bisa terpotong 2x. Dengan idempotent, yang kedua cuma di-skip — aman.

4. Jurnal Balanced

test "jurnal penjualan WAJIB balanced (debit = credit)" do
  ctx = setup_full!()
  order = create_order!(ctx)
  {:ok, result} = Orders.ship_order(order.id, "nelly@bestari.id")

  # Ambil jurnal yang baru dibuat
  journals = Accounting.list_journal_entries(
    ctx.org.id, source_type: "order", source_id: order.id)

  for je <- journals do
    lines = Accounting.list_journal_entry_lines(je.id)
    total_debit = Enum.reduce(lines, Decimal.new(0), &Decimal.add(&1.debit, &2))
    total_credit = Enum.reduce(lines, Decimal.new(0), &Decimal.add(&1.credit, &2))
    assert Decimal.eq?(total_debit, total_credit)
  end
end

Ini test paling penting di akuntansi. Jurnal yang gak balance = laporan keuangan salah = keputusan bisnis salah.


Menjalankan Test

Semua test sekaligus

$ mix test

Hanya file tertentu

# Test orders saja
$ mix test test/lababersih/orders/orders_test.exs

Hanya test tertentu (by line number)

# Test yang ada di baris 122
$ mix test test/lababersih/orders/orders_test.exs:122

Hanya test yang gagal sebelumnya

$ mix test --failed
Tip untuk leader Kamu gak perlu jalankan test sendiri. Claude yang jalankan. Tapi kamu perlu tau cara membaca output-nya (section berikutnya).

Cara Membaca Output Test

Kalau semua pass (target):

$ mix test
..................................

Finished in 4.2 seconds (0.1s async, 4.1s sync)
337 tests, 0 failures

Setiap titik (.) = 1 test yang pass. 0 failures = semua aman. Ini yang kamu mau lihat.

Kalau ada yang gagal:

$ mix test
............................F......

  1) test ship order → status + stok + jurnal (Lababersih.OrdersTest)
     test/lababersih/orders/orders_test.exs:122

     Assertion with == failed
     code:  assert product.stok == 98
     left:  100        ← NILAI YANG DIDAPAT
     right: 98         ← NILAI YANG DIHARAPKAN

Finished in 4.5 seconds
337 tests, 1 failure
Cara baca error ini
  1. F (huruf merah) = test yang gagal
  2. Nama test: "ship order → status + stok + jurnal" — kamu tau FUNGSI APA yang rusak
  3. File + baris: orders_test.exs:122 — tepat di mana
  4. left vs right: left: 100 (stok gak berkurang), right: 98 (harusnya berkurang). Berarti logic potong stok ada bug.
Pesan ke Claude: "Test ship_order gagal, stok gak berkurang dari 100 ke 98. Cek function consume_lots."

Test Coverage LabaBersih v2

Ini jumlah test per context module saat ini:

Context ModuleFile TestJumlah Test
Accounts (auth, org)accounts_test.exs + password_reset_test.exs35+
Accounting (COA, jurnal)accounting_test.exs + daily_journal_test.exs30
Orders (pesanan, packing)orders_test.exs + xlsx_import_test.exs + reservation_test.exs35
Inventory (produk, stok)inventory_test.exs + master_data_test.exs + warehouse_operations_test.exs53
Customers (pelanggan)customers_test.exs22
Returns (RTS, klaim)returns_test.exs + rts_management_test.exs + return_enhancement_test.exs28
Sales (toko, fee)sales_test.exs10
Purchasing (PO)purchasing_test.exs8
Reconciliationreconciliation_test.exs4
Fulfillmentfulfillment_test.exs11
Cross-domain / edge casesbusiness_cases_test.exs + advanced_test.exs + lainnya100+
TOTAL337+

Apa yang Perlu Di-test, Apa yang Tidak

WAJIB di-test:

GAK PERLU di-test:

Prinsip: test LOGIC kamu, bukan tool orang lain Kalau kamu bikin function calculate_hpp, test itu. Tapi jangan test apakah Decimal.add bisa nambah. Itu tanggung jawab library Decimal.

Credo — Code Quality Linter

Credo = alat yang cek kualitas kode secara otomatis. Dia gak jalankan kode, tapi baca kode dan cari yang gak rapi.

$ mix credo

Apa yang dicek Credo:

KategoriContoh temuanAnalogi
Consistency"Kadang pakai String.upcase, kadang String.downcase di tempat serupa"Consistency — seragam gak?
Readability"Function ini terlalu panjang (> 50 baris)"Kebersihan — rapih gak?
Refactoring"Variable ini gak pernah dipakai"Sampah — ada yang gak perlu?
Warning"Module belum punya @moduledoc"Dokumentasi — ada penjelasan?
Design"Function ini terlalu banyak parameter"Arsitektur — strukturnya bagus?

Contoh output:

$ mix credo

  Checking 84 source files ...Warnings - pleaseass these before submitting
┃
┃ [W] ↗ Modules should have a @moduledoc tag.
┃       lib/lababersih/workers/daily_sales_journal_worker.ex:1:20 #(Lababersih.Workers.DailySalesJournalWorker)

Analysis took 0.5s (0.03s to load, 0.47s running 52 checks on 84 files)
77 mods/funs, found 1 warning.
Untuk leader Credo itu seperti code reviewer otomatis. Dia gak cari bug, tapi cari kode yang jorok. Kode jorok hari ini = bug besok. Kalau Claude ngasih code dan Credo nemu 5 warnings, minta Claude fix dulu sebelum commit.

Sobelow — Security Scanner

Sobelow = alat yang cari celah keamanan di kode Phoenix.

$ mix sobelow

Apa yang dicek Sobelow:

KategoriContoh temuanDampak kalau gak di-fix
SQL InjectionQuery database pakai string concatenationHacker bisa baca/hapus semua data
XSSRender input user tanpa escapeHacker bisa inject script berbahaya
Hardcoded SecretAPI key langsung di kode, bukan env varSecret bocor ke GitHub
CSRFForm tanpa token CSRFHacker bisa kirim form atas nama user
Insecure ConfigSSL disabled, secret key lemahData gak terenkripsi
WAJIB clean sebelum deploy Credo warnings bisa ditunda. Sobelow findings HARUS di-fix sebelum deploy ke production. Security = non-negotiable. LabaBersih v2 sudah pass Sobelow clean.

3 Tools Quality, 3 Peran

ToolCommandCek apaAnalogi
ExUnitmix testApakah kode JALAN dengan benar?Ujian — jawaban benar?
Credomix credoApakah kode DITULIS dengan benar?Editor — tulisan rapi?
Sobelowmix sobelowApakah kode AMAN dari serangan?Security guard — ada celah?

Review Checklist untuk Leader

Setiap kali Claude bikin function baru atau update function lama, cek ini:

#PertanyaanCari di manaRed flag
1Ada test untuk function baru?File _test.exs yang sesuaiFunction ada, test gak ada
2Ada test happy path?test "... berhasil"Hanya test error, gak test sukses
3Ada test error path?test "... gagal" atau assert {:error, ...}Hanya test sukses, gak test gagal
4Ada test edge case?Idempotent, boundary, rollbackCuma 1 test untuk function kompleks
5mix test pass?Output: "X tests, 0 failures"Ada failures
6mix credo clean?Output: "found 0 issues"Banyak warnings
7Jurnal balanced di test?assert Decimal.eq?(total_debit, total_credit)Jurnal dibuat tapi gak dicek balance

Red Flags — Tanda Bahaya

Kalau kamu lihat hal-hal ini, tanya Claude:

1. Function tanpa test

# Ada function baru di orders.ex
def cancel_order(order_id) do
  # ... logic ...
end

# TAPI gak ada di orders_test.exs
# ← RED FLAG! Minta Claude bikin test-nya

2. Test tanpa assertion

test "ship order" do
  ctx = setup_full!()
  order = create_order!(ctx)
  Orders.ship_order(order.id, "nelly@bestari.id")
  # ← GIT GAK ADA assert! Test ini selalu pass.
  #    Gak berguna. Minta Claude tambah assertions.
end

3. Test yang terlalu umum

test "ship order berhasil" do
  ctx = setup_full!()
  order = create_order!(ctx)
  {:ok, _} = Orders.ship_order(order.id, "nelly@bestari.id")
  # Cuma cek {:ok, _} tapi gak cek stok, gak cek jurnal, gak cek status
  # ← RED FLAG! Ship order punya 5 side effect, semua harus dicek
end

4. Skip test

@tag :skip
test "ship order" do
  # ...
end
# ← RED FLAG! Test di-skip = test gak ada.
#    Tanya: kenapa di-skip? Kapan di-enable?
Rule absolut "Test yang gak pernah gagal = test yang gak berguna." Kalau test selalu pass apapun yang terjadi (gak ada assert, atau assert yang terlalu longgar), itu bukan test — itu hiasan.

Ringkasan Bab 7

Yang harus kamu ingat
  1. Test = asuransi. v1 = 0 test, bug ditemukan user. v2 = 337+ test, bug ditemukan mesin.
  2. mix test — 1 command, semua terverifikasi.
  3. 3 pola test: happy path (berhasil), error path (gagal dengan benar), edge case (idempotent).
  4. Output test: cari 0 failures. Kalau ada failure, baca left vs right.
  5. Credo = code reviewer otomatis. Sobelow = security scanner.
  6. Review: setiap function baru harus punya test. Gak ada pengecualian.
  7. Red flags: function tanpa test, test tanpa assert, test di-skip.

Pertanyaan sederhana yang bisa kamu tanya ke Claude setiap session:

"mix test masih pass semua?"
"Function baru ini sudah ada test-nya?"
"Ada happy path, error path, dan edge case?"

Tiga pertanyaan ini saja sudah cukup untuk menjaga kualitas kode LabaBersih.