Membangun dengan titik pemeriksaan

Membangun proyek bertahap dengan tes dan dokumentasi pada setiap tahap.

Daftar isi

Desain proyek suhu sudah tertulis, sehingga pekerjaan tinggal mengeksekusinya tanpa menghilangkan disiplin. Topik ini membangun proyek tersebut melalui tiga titik pemeriksaan (checkpoint), yaitu keadaan ketika satu bagian proyek dinyatakan selesai karena kodenya berjalan, tesnya lulus, dan dokumentasinya terbarui. Setiap titik menghasilkan satu bukti keluaran nyata, sehingga kemajuan proyek dapat diperiksa siapa pun tanpa perlu membaca seluruh kodenya. Praktik yang sama berlaku pada proyek perangkat lunak apa pun, hanya saja skala pemeriksaannya yang berbeda.

Rencana titik pemeriksaan#

Rencana pembangunan mengikuti tata letak modul pada desain, karena urutan yang aman adalah membangun dari bagian tanpa ketergantungan menuju titik masuk. Rencana lengkapnya digambarkan berikut:

Titik pemeriksaan 1parser.nimtes_parser.nimpengurai CSV terujiTitik pemeriksaan 2statistik.nimtes_statistik.nimperhitungan terujiTitik pemeriksaan 3suhu.nimnim docprogram utuh terujisetiap titik berakhir dengan tes lulus dan dokumentasi terbarui
Tiga titik pemeriksaan pembangunan proyek suhu

Titik pertama menyelesaikan parser.nim beserta pengujinya, titik kedua menyelesaikan statistik.nim beserta pengujinya, dan titik ketiga menyatukan semuanya di suhu.nim lengkap dengan dokumentasi HTML. Definisi selesai setiap titik dibuat spesifik agar pemeriksaannya objektif, yaitu kode baru terkompilasi, seluruh tes lulus, dan komentar dokumen terbarui sesuai keadaan kodenya. Bukti setiap titik berupa keluaran perintah, sehingga kemajuan dapat diarsipkan dan dibandingkan antarwaktu. Apabila satu titik gagal, pekerjaan berhenti pada titik itu sampai penyebabnya ditemukan, karena melanjutkan di atas fondasi yang bermasalah hanya memindahkan pencarian error ke tempat yang lebih sulit. Urutan ini juga membuat setiap tes hanya menguji kode baru, karena bagian yang lain sudah diloloskan pada titik sebelumnya, dan tes dijalankan bukan hanya di akhir titik, melainkan setiap kali kode disunting, sekecil apa pun perubahannya.

Titik pemeriksaan 1: pengurai CSV#

Modul pertama menerjemahkan kontrak proc parser menjadi kode lengkap. Setiap proc diberi komentar dokumen (doc comment), yaitu komentar berawalan dua pagar ## yang dipahami Nim sebagai dokumentasi resmi simbol tersebut, bukan sekadar catatan pembaca kode:

nim parser.nim
## Pengurai berkas CSV catatan suhu.
##
## Format yang didukung adalah CSV dua kolom dengan baris header
## `tanggal,suhu` dan satu catatan per baris.

import std/strutils

type
  CatatanSuhu* = object
    tanggal*: string
    suhu*: float

const
  headerCsv* = "tanggal,suhu"

proc uraiBaris*(baris: string): CatatanSuhu =
  ## Mengurai satu baris CSV menjadi nilai `CatatanSuhu`. Proc ini
  ## memunculkan `ValueError` bila jumlah kolomnya bukan dua atau
  ## kolom suhunya bukan angka yang sah.
  let kolom = baris.split(',')
  if kolom.len != 2:
    raise newException(ValueError, "baris harus dua kolom: " & baris)
  CatatanSuhu(tanggal: kolom[0], suhu: parseFloat(kolom[1].strip()))

proc uraiCsv*(isi: string): seq[CatatanSuhu] =
  ## Mengurai isi berkas CSV yang utuh. Baris header dan baris kosong
  ## dilewati, sedangkan baris yang cacat memunculkan `ValueError`.
  result = @[]
  for baris in isi.splitLines():
    if baris.strip() == "" or baris == headerCsv:
      continue
    result.add(uraiBaris(baris))

proc bacaCsv*(namaBerkas: string): seq[CatatanSuhu] =
  ## Membaca berkas CSV dari disk lalu mengurainya dengan `uraiCsv`.
  uraiCsv(readFile(namaBerkas))

Pengujinya menggunakan unittest dari pustaka standar, dengan suite mengelompokkan tes, test menyatakan satu kasus, check menegaskan kondisi yang harus benar, dan expect menegaskan bahwa blok tertentu memunculkan pengecualian. Pemanggilan di dalam expect diberi awalan discard karena nilai hasilnya memang tidak dipakai, sesuai aturan Nim yang melarang hasil proc terbuang tanpa sengaja. Kasus yang diuji diambil dari kontrak proc, termasuk dua kasus error yang dijanjikan memunculkan ValueError:

nim tes_parser.nim
## Pengujian modul parser dengan unittest.

import unittest
import parser

suite "urai baris":
  test "baris lengkap":
    let c = uraiBaris("2026-01-02,21.5")
    check c.tanggal == "2026-01-02"
    check c.suhu == 21.5

  test "spasi sebelum angka diabaikan":
    let c = uraiBaris("2026-01-03, 22.1")
    check c.suhu == 22.1

  test "kolom suhu bukan angka memunculkan ValueError":
    expect ValueError:
      discard uraiBaris("2026-01-04,abis")

suite "urai csv":
  test "header dan baris kosong dilewati":
    let isi = "tanggal,suhu\n\n2026-01-02,21.5\n2026-01-03,22.1\n"
    check uraiCsv(isi).len == 2
    check uraiCsv(isi)[0].tanggal == "2026-01-02"

  test "kolom kurang dari dua memunculkan ValueError":
    expect ValueError:
      discard uraiCsv("tanggal,suhu\n2026-01-04\n")

Karena uraiCsv menerima string, seluruh tes berjalan tanpa berkas fisik, persis seperti dirancang pada kontrak proc. Titik pertama dinyatakan selesai setelah perintah berikut lulus:

bash
# mengompilasi sekaligus menjalankan tes parser
nim c -r --hints:off tes_parser.nim
text Keluaran
[Suite] urai baris
  [OK] baris lengkap
  [OK] spasi sebelum angka diabaikan
  [OK] kolom suhu bukan angka memunculkan ValueError

[Suite] urai csv
  [OK] header dan baris kosong dilewati
  [OK] kolom kurang dari dua memunculkan ValueError

Keluaran menunjukkan lima tes dalam dua suite yang semuanya lulus, dan baris kosong menjadi pemisah alami antarsuite. Urutan keluarannya mengikuti urutan penulisan dalam berkas, sehingga kegagalan pertama mudah dilacak ke kasus yang persis. Titik pemeriksaan baru ditutup apabila keluaran berbentuk seperti ini, tanpa satu pun kegagalan yang terabaikan.

Titik pemeriksaan 2: statistik deret#

Modul kedua murni menghitung, tanpa IO sama sekali, persis seperti kontraknya. Perhatikan bahwa median mengurutkan salinan deret lewat sorted, sehingga deret milik pemanggil tidak ikut terurut, sebuah efek samping yang mudah luput dari perhatian bila tidak dirancang sejak awal. Keempat proc tidak mengetahui keberadaan CSV sama sekali karena desain memutus ketergantungan statistik pada parser. Pemisahan itu membuat modul statistik dapat dipakai ulang oleh sumber data lain tanpa mengubah satu baris pun, dan pengujiannya cukup menyusun deret kecil secara manual, sehingga kasus khusus seperti banyak data ganjil dan genap mudah dicakupi:

nim statistik.nim
## Statistik deskriptif untuk deret nilai suhu.
##
## Seluruh proc menerima `seq[float]` dan tidak melakukan IO apa pun,
## sehingga mudah diuji secara terpisah dari pembacaan berkas.

import std/algorithm

proc minimum*(data: seq[float]): float =
  ## Mengembalikan nilai terkecil pada deret. Deret asumsinya tidak kosong.
  result = data[0]
  for x in data:
    if x < result:
      result = x

proc maksimum*(data: seq[float]): float =
  ## Mengembalikan nilai terbesar pada deret. Deret asumsinya tidak kosong.
  result = data[0]
  for x in data:
    if x > result:
      result = x

proc rataRata*(data: seq[float]): float =
  ## Mengembalikan rata-rata hitung dari seluruh nilai pada deret.
  var total = 0.0
  for x in data:
    total += x
  total / data.len.float

proc median*(data: seq[float]): float =
  ## Mengembalikan nilai tengah deret. Bila banyak datanya genap,
  ## nilai tengahnya adalah rata-rata dua nilai tengah.
  let urut = sorted(data)
  let n = urut.len
  if n mod 2 == 1:
    urut[n div 2]
  else:
    (urut[n div 2 - 1] + urut[n div 2]) / 2.0

Tes statistik memakai nilai-nilai yang hasilnya pasti, seperti 20.0 dan 25.0, agar perbandingan == tidak menabrak pembulatan pecahan. Titik kedua selesai setelah tesnya lulus:

nim tes_statistik.nim
## Pengujian modul statistik dengan unittest.

import unittest
import statistik

suite "statistik deret":
  test "minimum dan maksimum":
    check minimum(@[21.5, 20.8, 23.4]) == 20.8
    check maksimum(@[21.5, 20.8, 23.4]) == 23.4

  test "rata-rata tiga nilai":
    check rataRata(@[10.0, 20.0, 30.0]) == 20.0

  test "median dengan banyak data ganjil":
    check median(@[30.0, 10.0, 20.0]) == 20.0

  test "median dengan banyak data genap":
    check median(@[10.0, 20.0, 30.0, 40.0]) == 25.0
bash
# mengompilasi sekaligus menjalankan tes statistik
nim c -r --hints:off tes_statistik.nim
text Keluaran
[Suite] statistik deret
  [OK] minimum dan maksimum
  [OK] rata-rata tiga nilai
  [OK] median dengan banyak data ganjil
  [OK] median dengan banyak data genap

Titik pemeriksaan 3: program utuh#

Titik terakhir menyatukan kedua modul di suhu.nim, yang sengaja dibuat tipis sesuai desain. Program ini membaca argumen lewat paramCount dan paramStr, menulis pesan error ke stderr, berakhir dengan kode keluar bukan nol melalui QuitFailure saat gagal, dan menangkap IOError maupun ValueError dari modul di bawahnya. Blok when isMainModule memastikan kode di dalamnya hanya berjalan ketika berkas ini dikompilasi sebagai program utama, sehingga berkas yang sama tetap aman diimpor oleh kode lain tanpa efek samping:

nim suhu.nim
## suhu - alat baris perintah untuk statistik catatan suhu berformat CSV.
##
## Pemakaian: suhu <berkas.csv>

import std/[os, strutils]
import parser, statistik

proc ambilSuhu(catatan: seq[CatatanSuhu]): seq[float] =
  ## Mengambil kolom suhu dari seluruh catatan menjadi deret nilai.
  result = newSeq[float](catatan.len)
  for i, c in catatan:
    result[i] = c.suhu

proc tampilkanLaporan(namaBerkas: string, catatan: seq[CatatanSuhu]) =
  ## Mencetak laporan statistik ke layar.
  let deret = ambilSuhu(catatan)
  echo "Berkas       : ", namaBerkas
  echo "Banyak data  : ", deret.len
  echo "Suhu minimum : ", formatFloat(minimum(deret), ffDecimal, 2)
  echo "Suhu maksimum: ", formatFloat(maksimum(deret), ffDecimal, 2)
  echo "Rata-rata    : ", formatFloat(rataRata(deret), ffDecimal, 2)
  echo "Median       : ", formatFloat(median(deret), ffDecimal, 2)

when isMainModule:
  if paramCount() != 1:
    stderr.writeLine("Pemakaian: suhu <berkas.csv>")
    quit(QuitFailure)
  try:
    let catatan = bacaCsv(paramStr(1))
    if catatan.len == 0:
      stderr.writeLine("Error: tidak ada data pada " & paramStr(1))
      quit(QuitFailure)
    tampilkanLaporan(paramStr(1), catatan)
  except IOError as e:
    stderr.writeLine("Error: " & e.msg)
    quit(QuitFailure)
  except ValueError as e:
    stderr.writeLine("Error: " & e.msg)
    quit(QuitFailure)

Kompilasi dan uji akhir memakai berkas data suhu.csv yang isinya persis seperti pada topik pemilihan proyek:

bash
# mengompilasi program utuh
nim c --hints:off suhu.nim
# menjalankan program dengan berkas data
./suhu suhu.csv
text Keluaran
Berkas       : suhu.csv
Banyak data  : 10
Suhu minimum : 20.50
Suhu maksimum: 24.00
Rata-rata    : 22.22
Median       : 22.20

Perilaku keluhan program juga diuji, karena laporan yang benar hanya separuh dari kontraknya. Enam baris laporan di atas adalah kontrak tampilan proyek, dan angkanya sudah diverifikasi silang pada prototipe. Pesan pemakai dan pesan error ditulis ke stderr, sehingga keduanya tidak tercampur dengan laporan di keluaran standar:

bash
# dijalankan tanpa argumen
./suhu
# dijalankan dengan berkas yang tidak ada
./suhu tidak-ada.csv
text Keluaran
Pemakaian: suhu <berkas.csv>
Error: cannot open: tidak-ada.csv

Kedua kasus gagal berakhir dengan kode keluar 1, bila perlu diperiksa lewat echo $?. Titik ketiga selesai dan seluruh proyek berada dalam keadaan yang berfungsi utuh.

Dokumentasi pada setiap titik#

Dokumentasi ikut diperbarui di setiap titik, bukan ditunda sampai programnya matang. Komentar dokumen ## pada setiap proc sudah ditulis bersama kodenya, dan Nim mengubahnya menjadi halaman HTML lewat perintah nim doc. Yang didokumentasikan bukan hanya proc, melainkan juga format berkas masukan, prasyarat proc, serta error yang mungkin muncul, karena bagian itulah yang dicari pengguna saat memakai modul. Halaman dokumentasi yang dihasilkan menjadi tempat menimbang perubahan kontrak sebelum kodenya disunting:

bash
# menghasilkan dokumentasi HTML dari komentar dokumen
nim doc --hints:off suhu.nim
# memeriksa isi direktori hasilnya
ls htmldocs
text Keluaran
nimdoc.out.css
suhu.html

Berkas suhu.html memuat deskripsi modul dari komentar dokumen di kepala berkas, beserta kontrak proc yang simbolnya diekspor. Perintah yang sama berlaku untuk parser.nim dan statistik.nim, sehingga dokumentasi seluruh modul tumbuh dari sumber yang sama tanpa berkas terpisah yang cepat usang. Dokumentasi yang dibangkitkan langsung dari kode selalu mengikuti perubahan terbaru, berbeda dari dokumen lepas yang mudah tertinggal. Dokumentasi yang tumbuh bersama kode juga memaksa kontraknya disebut secara eksplisit, dan ketidaksesuaian antara komentar dengan perilaku terlihat saat penulisan, bukan berbulan-bulan kemudian.

Kesalahan umum#

  • Menunda tes sampai seluruh program selesai. Kesalahan yang menumpuk menyulitkan pelacakan karena penyebabnya bisa di mana saja. Perbaikannya adalah mengikuti urutan titik pemeriksaan dan menganggap titik belum selesai selama tesnya belum lulus.
  • Menguji hanya jalur yang berhasil. Tes yang hanya menguji masukan benar meloloskan error yang justru paling sering ditemui pengguna, seperti berkas yang tidak ada. Perbaikannya adalah menuliskan kasus expect ValueError untuk setiap kontrak yang memunculkan pengecualian.
  • Membiarkan dokumentasi menyusul di akhir. Komentar dokumen yang ditulis jauh setelah kodenya sering tidak akurat karena ingatannya sudah pudar. Perbaikannya adalah memperlakukan dokumentasi sebagai bagian dari definisi selesai setiap titik.
  • Menyalin keluaran tes dari ingatan. Keluaran yang diketik ulang cenderung berbeda halus dari yang sebenarnya muncul. Perbaikannya adalah menyalin keluaran langsung dari terminal pada saat menjalankan tesnya.
  • Meloloskan tes yang tidak menegaskan apa pun. Tes tanpa check lulus selalu, sekalipun logikanya keliru, sehingga kepercayaan palsu tumbuh tanpa terlihat. Perbaikannya adalah memastikan setiap blok test memuat sekurang-kurangnya satu check atau expect.

Ringkasan#

  • Pembangunan proyek suhu dibagi menjadi tiga titik pemeriksaan, yaitu pengurai CSV, statistik deret, lalu program utuh dengan dokumentasi.
  • Setiap titik dinyatakan selesai hanya bila kode berjalan, tes unittest lulus, dan dokumentasinya terbarui.
  • Penguraian diuji dari string tanpa berkas fisik, dan statistik diuji dengan nilai yang hasilnya pasti agar perbandingan == aman.
  • Program utuhnya tipis, menulis pesan error ke stderr, berakhir dengan kode keluar bukan nol saat gagal, dan menghasilkan laporan enam baris untuk suhu.csv.

Referensi#