Pengujian transisi keadaan

Cara menguji sistem yang perilakunya bergantung pada status objek dan menemukan cacat dalam logika alur kerja.

Diperbarui:

Daftar isi

Pengujian transisi keadaan membantu ketika perilaku sistem bergantung pada keadaan objek saat ini. Pengguna, pesanan, tiket, pembayaran, langganan, dan sesi semuanya memiliki status dan transisi yang diizinkan.

Kapan menggunakannya

  • →Ada status eksplisit seperti dibuat, aktif, diblokir, dibatalkan, dan selesai.
  • →Beberapa transisi dilarang.
  • →Perilaku dan tindakan yang tersedia bergantung pada keadaan saat ini.
  • →Bug sering kali melibatkan pelanggaran siklus hidup, bukan bidang individual.

Apa yang harus didokumentasikan

  • →Status objek
  • →Transisi yang diizinkan
  • →Peristiwa yang memicu transisi
  • →Batasan: siapa yang dapat memulai transisi, dalam kondisi apa, dan apa yang terjadi setelahnya

Contoh

Misalkan laporan bug berstatus Terbuka, Sedang Berlangsung, Siap Diuji, Dibuka Kembali, dan Ditutup. Pengujian transisi status memeriksa apakah penutupan langsung diperbolehkan, siapa yang boleh membukanya kembali, dan apakah siklus perbaikan lainnya mempertahankan logika yang benar.

Apa yang harus diperiksa QA

  • →Transisi terlarang: sistem harus memblokirnya atau menjelaskannya dengan tepat.
  • →Efek samping transisi: notifikasi, audit, perubahan akses, dan pencatatan.
  • →Transisi berulang dan gerakan mundur.
  • →Status setelah kegagalan: apa yang terjadi jika suatu operasi dihentikan di tengah jalan?
🔁

Jika suatu objek memiliki siklus hidup, menggambar diagram keadaan sederhana biasanya berguna. Ini dengan cepat mengungkapkan titik-titik buta yang sulit dilihat dalam daftar kasus.

Contoh siklus hidup pesanan dengan kontrak eksplisit

Ini adalah model fiktif untuk satu pesanan, bukan perilaku setiap toko atau pembayaran API. Draft adalah pesanan yang belum terkirim, Placed telah dikirimkan, Paid telah dibayar, Shipped telah dikirim, dan Cancelled adalah terminal. Hanya lima transisi di bawah ini yang diperbolehkan. Pembatalan setelah pembayaran, pengembalian uang, izin, kesalahan transportasi, dan permintaan bersamaan berada di luar model kecil ini dan memerlukan aturan tambahan.

plain text
Draft --submit--> Placed --pay--> Paid --ship--> Shipped
Draft --cancel--> Cancelled
Placed --cancel--> Cancelled

Alternatif teks: submit memindahkan Draft ke Placed; pay memindahkan Placed ke Paid; ship memindahkan Paid ke Shipped. cancel memindahkan Draft atau Placed ke Cancelled. Shipped dan Cancelled tidak memiliki transisi keluar. Semua kombinasi keadaan/peristiwa lainnya ditolak tanpa mengubah keadaan atau menimbulkan efek samping bisnis.

Tabel pengujian peristiwa tunggal yang lengkap

Reset pesanan ke keadaan pada kolom pertama sebelum setiap baris dan gunakan kunci request baru. Jumlah tersebut adalah penghitung model untuk transisi bisnis yang tersimpan; bukan jumlah universal log, notifikasi, atau tagihan eksternal. Transisi yang diterima menambah satu; penolakan menambah nol.

plain text
Keadaan awal	Peristiwa	Hasil	Keadaan akhir	Efek baru
Draft	submit	diterima	Placed	1
Draft	pay	ditolak	Draft	0
Draft	ship	ditolak	Draft	0
Draft	cancel	diterima	Cancelled	1
Placed	submit	ditolak	Placed	0
Placed	pay	diterima	Paid	1
Placed	ship	ditolak	Placed	0
Placed	cancel	diterima	Cancelled	1
Paid	submit	ditolak	Paid	0
Paid	pay	ditolak	Paid	0
Paid	ship	diterima	Shipped	1
Paid	cancel	ditolak	Paid	0
Shipped	submit	ditolak	Shipped	0
Shipped	pay	ditolak	Shipped	0
Shipped	ship	ditolak	Shipped	0
Shipped	cancel	ditolak	Shipped	0
Cancelled	submit	ditolak	Cancelled	0
Cancelled	pay	ditolak	Cancelled	0
Cancelled	ship	ditolak	Cancelled	0
Cancelled	cancel	ditolak	Cancelled	0

Percobaan ulang adalah kontrak terpisah, bukan keadaan tambahan

Untuk model ini, permintaan yang berhasil menyimpan kejadian dan hasilnya di bawah kunci yang dicakup dalam pesanan ini. Mengulangi kunci dan peristiwa yang sama akan mengembalikan hasil yang disimpan tanpa efek samping baru. Menggunakan kembali kunci yang berhasil untuk peristiwa berbeda akan mengembalikan konflik kunci. Permintaan yang ditolak tidak disimpan. Aturan yang sengaja disederhanakan ini bukan merupakan pernyataan tentang kode status HTTP atau penyedia pembayaran sebenarnya.

plain text
Urutan	Respons yang diharapkan	Keadaan saat ini	Total efek yang tersimpan
submit / s1	diterima, Placed	Placed	1
pay / p1	diterima, Paid	Paid	2
pay / p1 lagi	diterima, Paid (hasil yang tersimpan)	Paid	2
ship / p1	key-conflict, Paid	Paid	2
pay / p2	ditolak, Paid	Paid	2
ship / s2	diterima, Shipped	Shipped	3

Hasil yang disimpan adalah cuplikan permintaan asli, belum tentu status pesanan saat ini setelah transisi selanjutnya. API asli harus menentukan cakupan kunci, retensi, perbandingan muatan, pemesanan, dan persistensi atom. Model pengajaran hanya memori tidak dapat menunjukkan perlindungan terhadap biaya duplikat secara bersamaan.

Jalankan model referensi secara lokal

Simpan sebagai order_state_model.py dan jalankan python3 order_state_model.py. Kode memeriksa seluruh 20 kombinasi keadaan/peristiwa, lalu urutan retry. Keluaran yang diharapkan: Transitions: 5 accepted, 15 rejected; replay checks: passed. Pemeriksaan ini memverifikasi model pembelajaran tersebut; uji aplikasi secara terpisah terhadap aturan yang sama.

plain text
STATES = ("Draft", "Placed", "Paid", "Shipped", "Cancelled")
EVENTS = ("submit", "pay", "ship", "cancel")
TRANSITIONS = {
    ("Draft", "submit"): "Placed", ("Draft", "cancel"): "Cancelled",
    ("Placed", "pay"): "Paid", ("Placed", "cancel"): "Cancelled",
    ("Paid", "ship"): "Shipped",
}
class Order:
    def __init__(self, state="Draft"):
        self.state, self.effects, self.saved = state, 0, {}
    def apply(self, event, key):
        if key in self.saved:
            old_event, old_result = self.saved[key]
            return old_result if event == old_event else ("key-conflict", self.state)
        target = TRANSITIONS.get((self.state, event))
        if target is None:
            return ("rejected", self.state)
        self.state = target
        self.effects += 1
        result = ("accepted", target)
        self.saved[key] = (event, result)
        return result

accepted = 0
for state in STATES:
    for event in EVENTS:
        order = Order(state)
        target = TRANSITIONS.get((state, event))
        expected = ("accepted", target) if target else ("rejected", state)
        assert order.apply(event, "unique") == expected
        assert order.state == (target or state)
        assert order.effects == int(target is not None)
        accepted += target is not None
order = Order()
assert order.apply("submit", "s1") == ("accepted", "Placed")
assert order.apply("pay", "p1") == ("accepted", "Paid")
assert order.apply("pay", "p1") == ("accepted", "Paid")
assert order.effects == 2
assert order.apply("ship", "p1") == ("key-conflict", "Paid")
assert order.apply("pay", "p2") == ("rejected", "Paid")
assert order.effects == 2
assert order.apply("ship", "s2") == ("accepted", "Shipped")
assert order.effects == 3
print(f"Transitions: {accepted} accepted, {20-accepted} rejected; replay checks: passed")

Untuk sistem nyata, perluas cakupan urutan: batas waktu antara pembayaran dan persistensi, panggilan balik duplikat, versi lama, dan pembatalan yang berpacu dengan pembayaran. Tentukan apa yang harus terjadi sebelum menulis pernyataan. Lima transisi yang tercakup tidak membuktikan semua jalur multi-langkah atau pemulihan kegagalan berhasil.

Lanjutkan belajar

Gunakan tabel keputusan untuk kondisi kondisi penjaga transisi

Periksa tindakan API yang ditolak tanpa efek samping

ISTQB CTFL 4.0.1: bagian 4.2.4, pengujian transisi keadaan