unittest dan testament

Menulis blok uji dengan unittest dan menjalankannya lewat runner testament.

Daftar isi

Program yang benar pada satu contoh masih bisa keliru pada masukan lain. Topik ini membahas dua perkakas pengujian bawaan Nim, yaitu modul unittest untuk menulis blok uji dan program testament sebagai runner, yakni program yang menjalankan kumpulan uji lalu merangkum hasilnya. Seluruh pembahasan memakai satu modul penghitung kata sebagai bahan uji, mulai dari menulis pemeriksaan, mengeksekusinya lewat kompilator, sampai membaca laporan runner tersebut.

Modul unittest menyediakan kerangka penulisan uji berupa suite, test, dan check. Suite mengelompokkan uji yang berkaitan, test membungkus satu kasus uji, dan check memeriksa satu ekspresi boolean serta menampilkan nilai kedua sisinya saat pemeriksaan gagal. Sebagai bahan uji, modul berikut memuat dua proc murni, yaitu jumlahKata dan kataTerpanjang, yang keduanya hanya membaca masukannya tanpa mengubah keadaan di luar dirinya. Tanda bintang pada namanya mengekspor proc agar dapat diimpor dari berkas lain, dan syarat itu penting karena berkas uji memperlakukan modul ini sebagai pustaka.

nim penghitung.nim
proc jumlahKata*(teks: string): int =
  ## Menghitung banyak kata yang dipisahkan spasi, tab, atau baris baru.
  result = 0
  var dalamKata = false
  for c in teks:
    if c in {' ', '\t', '\n'}:
      dalamKata = false
    elif not dalamKata:
      dalamKata = true
      inc result

proc kataTerpanjang*(teks: string): string =
  ## Mengembalikan kata pertama yang paling panjang.
  ## Memunculkan ValueError bila teks tidak memuat satu kata pun.
  var
    terpanjang = ""
    kataSaatIni = ""
  for c in teks & " ":
    if c in {' ', '\t', '\n'}:
      if kataSaatIni.len > terpanjang.len:
        terpanjang = kataSaatIni
      kataSaatIni = ""
    else:
      kataSaatIni.add(c)
  if terpanjang.len == 0:
    raise newException(ValueError, "teks tidak memuat kata")
  return terpanjang

Berkas uji berikut mengimpor unittest dan modulnya, lalu menulis lima kasus uji di dalam satu suite. Empat kasus pertama memeriksa nilai yang benar, sedangkan kasus kelima memeriksa perilaku error yang dibahas pada bagian Menguji error dan membaca kegagalan.

nim penghitung_uji.nim
import std/unittest
import penghitung

suite "penghitung kata":
  test "jumlah kata teks biasa":
    check jumlahKata("belajar nim itu menyenangkan") == 4

  test "jumlah kata dengan spasi berlebih":
    check jumlahKata("  satu   dua  ") == 2

  test "teks kosong tidak memiliki kata":
    check jumlahKata("") == 0

  test "kata terpanjang":
    check kataTerpanjang("belajar nim itu menyenangkan") == "menyenangkan"

  test "teks tanpa kata memunculkan error":
    expect ValueError:
      discard kataTerpanjang("   ")

Jalankan berkas uji tersebut seperti program Nim biasa dengan nim c -r. Bendera --verbosity:0 meredam pesan kompilasi agar keluaran hanya memuat laporan uji.

bash
# kompilasi lalu langsung jalankan berkas uji
nim c --verbosity:0 -r penghitung_uji.nim
text Keluaran
[Suite] penghitung kata
  [OK] jumlah kata teks biasa
  [OK] jumlah kata dengan spasi berlebih
  [OK] teks kosong tidak memiliki kata
  [OK] kata terpanjang
  [OK] teks tanpa kata memunculkan error

Setiap test dicetak satu baris dengan status [OK] di bawah judul suitenya, sehingga kasus yang gagal langsung terlihat namanya. Varian pemeriksaan yang lazim adalah check cond == nilai untuk perbandingan dan expect untuk error. Blok expect ValueError pada kasus kelima menganggap uji lulus hanya bila proc memunculkan error jenis itu, dan menganggapnya gagal bila error tidak muncul atau jenisnya berbeda. Penataan yang lebih lengkap masih tersedia, misalnya blok setup dan teardown yang dijalankan sebelum serta sesudah setiap test untuk menyiapkan dan membersihkan data uji.

Menguji error dan membaca kegagalan#

Pesan kegagalan check memuat lokasi persis beserta nilainya. Apabila satu pemeriksaan tidak terpenuhi, unittest mencetak baris berformat berkas(bar, kolom): Check failed: <ekspresi>, lalu menampilkan nilai yang diperoleh pada baris berikutnya. Contoh nyatanya berupa Check failed: 2 - 2 == 1 yang diikuti baris 2 - 2 was 0, sehingga penyebabnya dapat ditetapkan tanpa menambah perintah cetak apa pun. Pola yang sama berlaku untuk expect: kegagalan dilaporkan bersama jenis error yang diharapkan dan yang benar-benar muncul.

Kegagalan juga bersifat fatal bagi proses uji. Program uji yang memuat satu kegagalan pun diakhiri dengan kode keluar bukan nol, dan status itulah yang nantinya dibaca runner testament. Karena setiap test dijalankan berurutan dalam satu proses, kegagalan pada satu kasus tidak menghentikan kasus lainnya, sehingga satu eksekusi cukup untuk melihat seluruh kasus yang bermasalah.

Menjalankan uji dengan testament#

Testament adalah runner uji tingkat proyek yang dipakai untuk mengembangkan Nim itu sendiri. Ia menjalankan banyak berkas uji, mencatat hasil ke basis data, menyusun laporan HTML, dan mampu menguji lebih dari satu target backend. Letak berkas ujinya diatur ketat, yaitu di dalam folder tests/<kategori> dengan nama berawalan huruf t, misalnya tests/dasar/tpenghitung.nim. Karena berkas uji berada dua tingkat di bawah akar proyek, modul yang diuji diimpornya dengan jalur relatif import ../../penghitung.

Berkas pertama memuat satu pemeriksaan yang sengaja keliru. Proc kataTerpanjang mengembalikan kata pertama saat panjangnya seri, sedangkan uji di bawah ini menyangka kata terakhir yang dikembalikan.

nim tests/dasar/tseri.nim
import std/unittest
import ../../penghitung

suite "kata dengan panjang sama":
  test "kata terpanjang saat seri":
    check kataTerpanjang("bumi biru") == "biru"
bash
# jalankan satu berkas uji dan tampilkan baris ringkasannya saja
testament --colors:off r tests/dasar/tseri.nim 2>&1 | tail -n 1
text Keluaran
FAILURE! total: 1 passed: 0 skipped: 0 failed: 1

Ringkasan testament dihitung per berkas, sehingga berkas yang memuat sembilan uji lulus dan satu uji gagal tetap tercatat sebagai satu kegagalan. Laporan lengkapnya memuat pesan Check failed beserta perbandingan hasil yang diharapkan dengan yang diperoleh, tetapi laporan itu juga memuat waktu eksekusi dan jalur instalasi Nim yang berubah antar eksekusi, sehingga hanya baris ringkasannya yang ditampilkan di sini. Perbaikan ujinya pun jelas, yaitu mengganti sisi kanan perbandingan menjadi "bumi".

Berkas kedua memuat uji yang lulus seluruhnya.

nim tests/dasar/tpenghitung.nim
import std/unittest
import ../../penghitung

suite "penghitung kata":
  test "jumlah kata teks biasa":
    check jumlahKata("belajar nim itu menyenangkan") == 4

  test "spasi berlebih dihitung satu":
    check jumlahKata("  satu   dua  ") == 2
bash
# jalankan berkas uji yang lulus dan tampilkan baris statusnya saja
testament --colors:off r tests/dasar/tpenghitung.nim 2>&1 | head -n 1 | sed 's/( *[0-9.]* sec)//'
text Keluaran
PASS: tests/dasar/tpenghitung.nim c

Baris PASS memuat nama berkas beserta target backend yang dipakai, yaitu c. Perintah testament pattern "tests/dasar/*.nim" menjalankan seluruh berkas yang cocok dengan polanya dalam satu kali eksekusi, sedangkan testament html menyusun berkas testresults.html dari basis data hasil agar tren kegagalan dapat ditinjau lewat browser. Target uji lain dapat dipilih dengan opsi --targets, misalnya menjalankan uji yang sama pada backend JavaScript tanpa mengubah satu baris pun dari berkas ujinya.

Kesalahan umum#

Kesalahan pertama berupa meletakkan berkas uji di luar konvensinya. Pemanggilan testament r pada berkas bernama uji1.nim di akar proyek diakhiri pesan fatal file must match this pattern: '/pathto/tests/dir/**/tfile.nim', dan perbaikannya adalah memindahkan berkas ke folder kategori di bawah tests sambil menambahkan awalan t pada namanya. Konvensi ini berlaku sejak pemanggilan pertama, jadi kebiasaan menamai berkas uji dengan rapi sejak awal lebih murah daripada memindahkannya kemudian.

Kesalahan kedua berupa memakai assert sebagai pengganti check. Pernyataan assert memang menghentikan program saat terpenuhi kebalikannya, tetapi laporannya tidak terikat pada suite dan test sehingga ringkasan per kasus tidak terbentuk, dan pada build -d:danger seluruh pemeriksaan assert dilewati sama sekali. Pemeriksaan check tetap bekerja pada semua mode build dan menghasilkan laporan per kasus, sehingga penggunaannya lebih tepat untuk uji.

Kesalahan ketiga berupa lupa mengekspor proc yang diuji. Proc tanpa tanda bintang bersifat privat bagi modulnya, sehingga berkas uji yang mengimpornya berhenti dengan pesan Error: undeclared identifier: 'jumlahKata'. Perbaikannya adalah menambahkan tanda bintang pada deklarasi proc di modul, bukan menyalin procnya ke berkas uji, karena uji seharusnya menguji kode yang sama dengan yang dipakai program utama.

Ringkasan#

  • unittest menata uji sebagai suite dan test, dengan check untuk memeriksa ekspresi beserta nilai kedua sisinya saat gagal dan expect untuk memastikan error jenis tertentu dimunculkan.
  • Berkas uji dijalankan seperti program biasa dengan nim c -r, setiap kasus dilaporkan dengan status [OK] atau [FAILED], dan kegagalan mengakhiri program dengan kode keluar bukan nol.
  • testament menjalankan uji dari pola tests/<kategori>/t*.nim, merangkum hasil per berkas, menjalankan banyak berkas lewat pattern, dan menyusun laporan HTML lewat html.
  • Baris PASS dan FAIL testament menjadi patokan cepat status uji, sedangkan laporan lengkapnya memuat detail kegagalan, waktu eksekusi, dan target backend.

Referensi#