Strategi test untuk wallet identitas digital dan dependensi platform tidak bisa mengandalkan end-to-end test semata. Begitu alur verifikasi identitas, biometrik, device binding, push notification, deep link, dan attestation bergantung pada layanan Google atau Apple, test akan mudah menjadi flaky jika batas antara kode yang kita kontrol dan dependency eksternal tidak dirancang dengan jelas.

Pendekatan yang lebih stabil adalah membagi pengujian berdasarkan risiko dan titik integrasi: logika inti diuji di unit test, orkestrasi diuji di integration test, kontrak ke layanan eksternal diuji secara eksplisit, dan hanya alur paling kritis yang diuji end-to-end pada perangkat nyata. Artikel ini membahas cara menyusun strategi tersebut, termasuk kapan boleh melakukan mocking, kapan harus memakai perangkat nyata, bagaimana menerapkan retry tanpa menutupi bug, dan seperti apa workflow CI untuk release verification.

Mengapa wallet identitas digital lebih sulit diuji

Wallet identitas digital biasanya tidak hanya menyimpan token atau profil pengguna. Ia sering menggabungkan beberapa komponen yang memiliki perilaku asinkron, pembatasan platform, dan faktor keamanan perangkat:

  • Verifikasi identitas yang memerlukan redirect, QR flow, atau callback lintas aplikasi.
  • Biometrik yang bergantung pada secure enclave, keystore, atau prompt OS.
  • Device binding yang mengikat kredensial ke perangkat tertentu.
  • Push notification untuk challenge, approval, atau status perubahan.
  • Deep link / universal link / app link untuk melanjutkan alur dari browser atau aplikasi lain.
  • Attestation untuk menilai integritas perangkat atau aplikasi.

Semua komponen itu punya karakteristik yang membuat test rapuh:

  • Respons dari platform bisa berubah atau tertunda.
  • State perangkat tidak selalu dapat direproduksi di emulator.
  • Event datang tidak sinkron, misalnya push masuk sebelum layar siap.
  • Kegagalan bisa berasal dari jaringan, konfigurasi bundle/app ID, sertifikat, clock skew, hingga race condition internal.

Masalah utama dalam testing wallet identitas digital bukan hanya “apakah fitur bekerja”, tetapi “apakah kegagalan bisa dibedakan antara bug aplikasi, masalah lingkungan, dan perilaku dependency eksternal”.

Prinsip arsitektur test: pisahkan logika domain dari adapter platform

Sebelum membahas jenis test, ada satu prinsip desain yang sangat menentukan kualitas test: jangan campurkan logika domain dengan API platform secara langsung.

Contoh buruk:

  • UI screen langsung memanggil API biometrik, memproses hasilnya, lalu menyimpan state verifikasi.
  • Handler push langsung memodifikasi credential state tanpa lapisan orkestrasi.
  • Deep link parser tersebar di banyak screen dan service.

Contoh yang lebih aman:

  • Domain layer: aturan bisnis wallet, status credential, policy retry, keputusan kapan rebind device diperlukan.
  • Application/service layer: orkestrasi use case seperti issue credential, verify presentation, rotate binding.
  • Adapter/platform layer: integrasi ke biometrik, push provider, attestation SDK, secure storage, deep link handler.

Dengan pemisahan ini, sebagian besar skenario kompleks bisa diuji tanpa menyentuh API Google/Apple secara langsung. Hasilnya:

  • Lebih sedikit flaky test.
  • Akar masalah lebih cepat ditemukan.
  • Perubahan SDK platform tidak langsung memecahkan seluruh suite test.

Test pyramid yang realistis untuk wallet identitas digital

1. Unit test untuk logika inti

Unit test seharusnya menutup sebagian besar kombinasi aturan bisnis yang tidak membutuhkan perangkat nyata. Fokuskan pada:

  • State machine alur issuance dan verification.
  • Keputusan kapan challenge kadaluarsa.
  • Validasi payload deep link yang sudah diparse.
  • Kebijakan fallback bila attestation gagal atau tidak tersedia.
  • Aturan device binding: kapan kredensial dianggap invalid, suspended, atau perlu rebind.

Contoh yang layak diuji di level unit:

  • Jika push challenge diterima setelah session ditutup, status harus diabaikan.
  • Jika attestation verdict adalah unknown, aplikasi tidak otomatis menganggap compromised kecuali policy memang mengharuskannya.
  • Jika deep link berisi nonce yang tidak cocok, flow dibatalkan dan audit event dicatat.

2. Integration test untuk orkestrasi dan boundary internal

Integration test memverifikasi modul-modul internal bekerja bersama dengan dependency yang dikontrol. Ini level terpenting untuk wallet identitas digital, karena di sinilah Anda dapat menguji orkestrasi tanpa ketidakstabilan perangkat nyata.

Contoh integration test:

  • Use case verifikasi memanggil parser deep link, validator nonce, secure storage, dan state repository dengan urutan yang benar.
  • Push handler memproses challenge lalu memicu refresh credential hanya jika device binding masih valid.
  • Respons attestation diterjemahkan ke risk score internal dengan benar.

Dependency eksternal pada level ini biasanya digantikan dengan test double yang memiliki kontrak ketat, bukan mock serba-bebas.

3. Contract test untuk integrasi eksternal

Contract test memastikan asumsi Anda terhadap API eksternal tetap benar. Ini sangat penting jika backend wallet bergantung pada layanan platform, identity provider, atau gateway push.

Contract test cocok untuk:

  • Schema request/response API verifikasi atau issuance.
  • Status code dan struktur error dari endpoint attestation verifier.
  • Payload push yang diterima backend lalu diteruskan ke mobile.
  • Format deep link callback yang diproduksi oleh web flow atau partner app.

Tanpa contract test, perubahan kecil seperti field yang menjadi opsional, enum baru, atau perubahan format timestamp dapat lolos hingga production.

4. End-to-end test untuk jalur kritis saja

Jangan memindahkan semua skenario ke E2E. Pada wallet identitas digital, E2E paling mahal, paling lambat, dan paling rawan false negative. Gunakan hanya untuk jalur yang benar-benar membutuhkan validasi antarkomponen nyata.

Skenario yang biasanya wajib diuji end-to-end:

  • Instalasi baru → registrasi perangkat → biometrik aktif → issuance kredensial sukses.
  • Flow verifikasi yang melibatkan deep link/universal link dari browser ke aplikasi.
  • Push challenge diterima pada aplikasi foreground dan background.
  • Device binding/attestation saat release candidate dipasang pada perangkat nyata.
  • Upgrade dari versi aplikasi sebelumnya tanpa merusak secure storage dan credential state.

Skenario yang biasanya cukup di integration test:

  • Mapping error dari SDK biometrik ke error domain internal.
  • Validasi timeout, retry policy, dan transisi state.
  • Pemrosesan payload push setelah payload sudah diterima aplikasi.
  • Logika policy berdasarkan verdict attestation.

Mocking yang aman: mock boundary, bukan perilaku bisnis

Kesalahan umum adalah mem-mock terlalu dalam sampai test hanya membuktikan implementasi internal hari ini, bukan perilaku sistem. Pada wallet identitas digital, mocking yang aman berarti:

  • Mock dependency eksternal di batas adapter.
  • Jangan mock aturan domain yang sedang diuji.
  • Gunakan fake yang meniru kegagalan realistis, bukan hanya respons sukses.

Contoh dependency yang wajar dimock/fake:

  • Client attestation verifier.
  • Push delivery callback ke backend test.
  • Biometric prompt adapter.
  • Secure storage adapter.
  • Deep link source atau redirect handler.

Contoh dependency yang sebaiknya tidak dimock ketika menguji orkestrasi:

  • State machine verifikasi.
  • Policy engine untuk risk decision.
  • Validator nonce/signature internal.

Berikut contoh interface adapter yang membuat boundary test lebih jelas:

interface AttestationService {
  suspend fun verify(attestationToken: String, challenge: String): AttestationResult
}

data class AttestationResult(
  val status: Status,
  val deviceIntegrity: DeviceIntegrity,
  val appIntegrity: AppIntegrity,
  val errorCode: String? = null
)

class VerifyDeviceUseCase(
  private val attestationService: AttestationService,
  private val bindingRepository: BindingRepository,
  private val policy: DeviceTrustPolicy
) {
  suspend fun execute(token: String, challenge: String): VerificationOutcome {
    val result = attestationService.verify(token, challenge)
    val decision = policy.evaluate(result)
    if (decision.allow) {
      bindingRepository.markVerified(challenge)
      return VerificationOutcome.Allowed
    }
    return VerificationOutcome.Blocked(decision.reason)
  }
}

Pada unit atau integration test, Anda bisa mengganti AttestationService dengan fake yang mengembalikan kombinasi hasil realistis, tanpa perlu memanggil layanan platform asli.

Kontrak API: cara mencegah regresi yang sulit dilacak

Dependency platform jarang berdiri sendiri. Biasanya ada backend yang menjadi perantara antara mobile wallet, identity provider, dan layanan verifikasi. Di sinilah contract test menjadi penting.

Apa yang perlu dikunci dalam kontrak

  • Field wajib vs opsional.
  • Enum yang mungkin bertambah nilainya.
  • Idempotency key untuk retry aman.
  • Format waktu, nonce, correlation ID, dan signature envelope.
  • Struktur error yang dipakai UI untuk mengambil keputusan.

Contoh kegagalan yang sering terjadi

  • Backend mengubah expires_at dari integer epoch menjadi string ISO, lalu mobile diam-diam gagal parse.
  • Provider menambahkan status baru seperti pending_review, tetapi aplikasi menganggapnya sukses.
  • Push callback tidak lagi mengirim correlation_id, sehingga challenge tidak bisa dipetakan ke session.

Contract test tidak harus rumit. Bahkan validasi schema sederhana di pipeline sudah membantu. Yang penting, kontrak dijalankan terhadap provider stub atau environment integrasi yang dikontrol, dan hasilnya menjadi syarat merge atau release.

Emulator vs perangkat nyata: kapan memakai masing-masing

Emulator atau simulator cocok untuk

  • Pengujian UI flow dasar.
  • Parser deep link dan navigasi internal.
  • State recovery setelah rotasi layar, kill app, atau restart proses yang bisa disimulasikan.
  • Sebagian integration test aplikasi mobile dengan fake adapter.

Kelebihannya adalah cepat, murah, dan mudah diotomasi. Namun emulator sering tidak cukup untuk kasus:

  • Biometrik asli.
  • Hardware-backed key storage.
  • Attestation yang memerlukan sinyal integritas perangkat nyata.
  • Perilaku push pada kondisi background/terminated yang bergantung pada OS.
  • Universal link/app link yang sensitif terhadap konfigurasi sertifikat dan domain association.

Perangkat nyata wajib untuk

  • Validasi biometrik dan fallback PIN/passcode OS.
  • Device binding yang memakai secure hardware atau keystore nyata.
  • Attestation end-to-end.
  • Push notification dalam berbagai state aplikasi: foreground, background, terminated.
  • Upgrade aplikasi lintas versi dengan data aman yang sudah ada.

Praktiknya, buat dua jalur:

  • Fast lane: emulator + fake/stub untuk validasi harian pada setiap commit.
  • Confidence lane: perangkat nyata untuk nightly build, candidate release, dan hotfix sensitif.

Risk-based test matrix: jangan uji semua kombinasi secara membabi buta

Karena kombinasi perangkat, OS, jaringan, dan status aplikasi sangat banyak, strategi yang masuk akal adalah test matrix berbasis risiko.

Dimensi yang relevan

  • Platform: Android, iOS.
  • Tipe perangkat: kelas low-end vs high-end bila relevan terhadap performa dan lifecycle.
  • Status aplikasi: foreground, background, terminated.
  • Kondisi jaringan: stabil, lambat, putus sementara.
  • Status perangkat: biometrik aktif/nonaktif, jam perangkat melenceng, storage hampir penuh.
  • Status akun/kredensial: baru, aktif, kadaluarsa, revoked, rebind required.

Cara memilih kombinasi prioritas

  1. Tentukan alur yang berisiko tinggi terhadap keamanan atau konversi pengguna.
  2. Tambahkan variasi hanya pada dimensi yang benar-benar memengaruhi alur tersebut.
  3. Uji jalur sukses dan beberapa jalur gagal yang paling mahal dampaknya.

Contoh matriks prioritas:

  • P0: issuance baru, verifikasi via deep link, push challenge, attestation saat login ulang.
  • P1: upgrade app, pergantian jaringan, session timeout, biometrik dibatalkan pengguna.
  • P2: locale berbeda, orientasi layar, notifikasi tertunda beberapa detik.

Dengan pendekatan ini, tim tidak terjebak mencoba mengotomasi semua kemungkinan, tetapi tetap menutup risiko regresi yang paling realistis.

Retry yang benar: memperbaiki ketidakstabilan, bukan menyamarkan bug

Retry sering dipakai untuk test yang bergantung pada jaringan atau event asinkron. Masalahnya, retry yang salah akan mengubah bug deterministik menjadi “kadang hijau”.

Retry boleh dipakai untuk

  • Polling event yang memang eventual, misalnya menunggu push diproses backend test.
  • Operasi jaringan non-deterministik dengan syarat ada idempotency.
  • Sinkronisasi terhadap state perangkat yang butuh waktu konsisten, misalnya menunggu app kembali aktif setelah deep link.

Retry tidak boleh dipakai untuk

  • Menutupi assertion yang sering gagal tanpa akar masalah jelas.
  • Memanggil ulang operasi yang tidak idempotent, seperti issuance yang bisa membuat credential ganda.
  • Menunggu elemen UI muncul tanpa event atau kondisi yang dapat diamati.

Prinsip implementasi retry pada test:

  • Retry hanya pada boundary yang diketahui tidak sinkron.
  • Gunakan timeout total kecil dan eksplisit.
  • Log setiap attempt dengan correlation ID.
  • Bedakan transient failure dari business failure.
async function waitForChallengeStatus(apiClient, challengeId, timeoutMs = 10000) {
  const started = Date.now();
  let lastStatus = null;

  while (Date.now() - started < timeoutMs) {
    const result = await apiClient.getChallengeStatus(challengeId);
    lastStatus = result.status;

    if (result.status === 'approved') return result;
    if (result.status === 'rejected' || result.status === 'expired') {
      throw new Error(`Business failure: ${result.status}`);
    }

    await new Promise(r => setTimeout(r, 500));
  }

  throw new Error(`Timeout waiting challenge status, last=${lastStatus}`);
}

Pola di atas lebih aman daripada sekadar me-retry seluruh test case, karena hanya menunggu kondisi eventual yang memang diharapkan.

Skenario gagal yang wajib dimiliki dalam suite test

Test untuk wallet identitas digital sering bias ke jalur sukses. Padahal regresi besar justru muncul dari penanganan gagal yang tidak lengkap. Berikut skenario gagal yang layak diprioritaskan:

Verifikasi dan deep link

  • Deep link diterima dua kali dan aplikasi harus tetap idempotent.
  • Nonce pada callback tidak cocok dengan session aktif.
  • Pengguna kembali ke aplikasi setelah session di server sudah kadaluarsa.
  • Universal link gagal terbuka dan fallback browser harus jelas.

Biometrik

  • Pengguna membatalkan prompt biometrik.
  • Biometrik tidak tersedia meski sebelumnya aktif.
  • Enrollment biometrik berubah dan kunci lokal menjadi tidak valid.

Device binding dan attestation

  • Attestation timeout, tetapi aplikasi tidak salah memberi status "trusted".
  • Verdict attestation tidak dikenal oleh client versi lama.
  • Binding token sudah pernah dipakai ulang.
  • Perangkat berganti atau restore backup menyebabkan kunci lokal tidak cocok.

Push notification

  • Push datang saat aplikasi terminated dan diproses terlambat.
  • Push diterima sebelum session lokal selesai dibuat.
  • Push payload kehilangan field korelasi.
  • Pengguna menonaktifkan notifikasi dan flow harus fallback ke polling atau manual refresh bila desain mendukung.

Workflow CI untuk release verification

CI untuk aplikasi seperti ini sebaiknya berlapis. Tujuannya bukan hanya cepat, tetapi juga menghasilkan sinyal yang bisa dipercaya.

Tahap 1: setiap pull request

  • Lint dan static analysis.
  • Unit test penuh.
  • Integration test dengan fake/stub dependency.
  • Contract test terhadap schema dan error mapping.

Tahap ini harus cepat dan stabil. Kalau sering merah karena lingkungan, developer akan mulai mengabaikannya.

Tahap 2: merge ke main atau nightly

  • Build aplikasi signed untuk environment test.
  • Automated UI test pada emulator/simulator.
  • Subset E2E terhadap backend test yang stabil.
  • Verifikasi migrasi storage atau upgrade dari build sebelumnya.

Tahap 3: release candidate verification

  • Jalankan E2E prioritas tinggi pada perangkat nyata.
  • Verifikasi biometrik, push, deep link, dan attestation.
  • Uji beberapa skenario gagal berisiko tinggi.
  • Pastikan environment, sertifikat, app association, dan konfigurasi push sesuai produksi.

Hal yang sering dilupakan di CI

  • Test observability: simpan log client, server, correlation ID, dan screenshot/video untuk E2E.
  • Data isolation: setiap run memakai account, challenge, dan device registration yang terisolasi.
  • Clock control: minimalkan perbedaan waktu antarserver dan perangkat test.
  • Feature flag awareness: jalur test harus eksplisit terhadap flag yang aktif.

Jika memungkinkan, pisahkan status “test gagal karena bug” dari “test tidak valid karena environment”. Kategori ini penting untuk triase cepat.

Menentukan apa yang wajib diuji end-to-end vs integration test

Pertanyaan praktis yang sering muncul: fitur ini harus E2E atau cukup integration test? Gunakan aturan sederhana berikut.

Wajib E2E jika

  • Fitur melibatkan lebih dari satu boundary eksternal nyata, misalnya browser → app link → app → push → backend.
  • Keberhasilan bergantung pada perilaku OS atau hardware.
  • Kegagalan di produksi biasanya berasal dari konfigurasi platform, bukan logika aplikasi saja.
  • Fitur berhubungan langsung dengan keamanan identitas atau akses kredensial.

Cukup integration test jika

  • Yang diuji adalah keputusan bisnis setelah respons dependency diterima.
  • Dependency bisa direpresentasikan dengan kontrak yang stabil.
  • Tidak ada kebutuhan untuk memverifikasi lifecycle OS, hardware, atau routing sistem.
  • E2E tidak akan memberi sinyal tambahan selain membuktikan hal yang sudah diverifikasi di bawahnya.

Jika ragu, tanyakan: apa jenis bug yang ingin ditangkap? Jika bug itu kemungkinan besar berupa mapping state, keputusan policy, atau orkestrasi internal, integration test biasanya cukup. Jika bug itu terkait sertifikat, delivery push, secure hardware, atau resolver deep link OS, E2E lebih tepat.

Checklist implementasi

  1. Definisikan boundary jelas antara domain wallet dan adapter platform.
  2. Buat state machine untuk issuance, verification, challenge, dan revocation agar dapat diuji deterministik.
  3. Tambahkan contract test untuk semua API yang memengaruhi alur verifikasi dan binding.
  4. Gunakan fake adapter untuk biometrik, push, attestation, dan secure storage di integration test.
  5. Bangun risk-based test matrix, bukan matriks kombinasi penuh.
  6. Tetapkan daftar skenario E2E minimum pada perangkat nyata sebelum release.
  7. Terapkan retry hanya pada event eventual dan operasi idempotent.
  8. Simpan correlation ID di client dan server untuk memudahkan debugging lintas komponen.
  9. Pastikan data test terisolasi dan mudah di-reset.
  10. Verifikasi jalur gagal secara eksplisit, bukan hanya happy path.
  11. Jalankan release verification pada konfigurasi yang semirip mungkin dengan produksi.

Debugging tips saat test mulai flaky

  • Periksa korelasi event: apakah push, deep link, dan session lokal memakai ID yang sama?
  • Pisahkan timeout UI dari timeout bisnis: layar belum update tidak selalu berarti proses bisnis gagal.
  • Catat state aplikasi saat menerima push: foreground, background, atau terminated.
  • Bandingkan emulator dan perangkat nyata: jika hanya gagal di perangkat nyata, curigai keystore, biometrik, app link, atau attestation.
  • Audit retry: test yang hijau setelah 3 retry sering menandakan race condition yang belum selesai.
  • Periksa perubahan kontrak diam-diam: field opsional baru, enum baru, atau perubahan format timestamp sangat sering jadi sumber regresi.

Penutup

Pada aplikasi wallet identitas digital, stabilitas test tidak datang dari menambah lebih banyak E2E, melainkan dari desain boundary yang tepat dan pemilihan level test yang sesuai. Strategi test untuk wallet identitas digital dan dependensi platform yang efektif biasanya menggabungkan unit test untuk logika domain, integration test untuk orkestrasi, contract test untuk API eksternal, dan E2E terbatas untuk alur yang benar-benar bergantung pada perangkat nyata, push, deep link, biometrik, dan attestation.

Jika Anda berhasil membedakan apa yang harus diuji secara deterministik dan apa yang memang perlu divalidasi di lingkungan nyata, flaky test akan berkurang tanpa mengorbankan cakupan risiko. Itu jauh lebih bernilai daripada suite besar yang sering gagal tetapi tidak membantu menemukan akar masalah.