Bab 18 — Testing Dasar: Memeriksa Kode Otomatis dengan pytest
Seri I1 — Kode yang Rapi dan Terawat
Semua skrip bab ini ada di folder kode/bab18/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Bayangkan Anda mengganti rumus biomassa di kelas Kolam. Apakah kandang, sawah, dan plot hutan masih menghasilkan angka yang benar? Memeriksa semuanya dengan mata tiap kali ada perubahan itu melelahkan dan pasti ada yang terlewat. Solusinya: tulis tes otomatis, yaitu kode kecil yang memanggil kode Anda lalu membandingkan hasilnya dengan jawaban yang diharapkan. Seperti timbangan ikan yang dikalibrasi tiap pagi: sekali disiapkan, ia memeriksa dalam sekejap.

assert: “pastikan ini benar”
Dasar semua tes di Python adalah assert. Bila syaratnya benar, program lanjut diam-diam. Bila salah, muncul AssertionError (error “pernyataan tidak terpenuhi”).
01_assert_dasar.py
# Bab 18 - assert: "pastikan ini benar, kalau tidak, berhenti"
biomassa_kg = 800 * 540.5 / 1000
assert biomassa_kg > 0
print("assert pertama lolos: biomassa", biomassa_kg, "kg")
try:
assert biomassa_kg == 400, "biomassa seharusnya 400 kg"
except AssertionError as e:
print("assert kedua gagal:", e)
jumlah = 0
try:
assert jumlah > 0
except AssertionError:
print("assert tanpa pesan juga bisa gagal")assert pertama lolos: biomassa 432.4 kg
assert kedua gagal: biomassa seharusnya 400 kg
assert tanpa pesan juga bisa gagal
Pernyataan pertama lolos tanpa suara selain print sesudahnya. Pada yang kedua, tanda koma menambahkan pesan yang muncul saat gagal. Skrip menangkapnya dengan try/except (Bab 12) supaya program tidak berhenti.
pytest dan tes pertama
pytest adalah pustaka tes yang paling banyak dipakai pemrogram Python. Ia sudah terpasang di environment buku ini; di komputer Anda, pasang dengan pip install pytest di dalam venv (Bab 17). Aturannya sederhana:
- Berkas tes bernama
test_*.py(atau*_test.py). - Fungsi tes bernama
test_.... - Di dalamnya, tulis
assert.
(Sumber: dokumentasi resmi pytest, halaman “Good Integration Practices”, dibaca 2026-10-10.)
02_tes_pertama.py
# Bab 18 - Tes pytest pertama: berkas test_*.py berisi fungsi test_*
from pathlib import Path
from bantu import FOLDER, jalankan_pytest
print(Path(FOLDER / "contoh_sederhana" / "test_hitung.py").read_text(encoding="utf-8"))
print("--- hasil pytest ---")
teks, kode = jalankan_pytest(["-v"], FOLDER / "contoh_sederhana")
print(teks)
print("kode keluar:", kode)def biomassa_kg(jumlah, bobot_g):
return jumlah * bobot_g / 1000
def test_biomassa_sederhana():
assert biomassa_kg(800, 500) == 400
def test_biomassa_nol():
assert biomassa_kg(0, 540.5) == 0
--- hasil pytest ---
============================= test session starts =============================
collecting ... collected 2 items
test_hitung.py::test_biomassa_sederhana PASSED [ 50%]
test_hitung.py::test_biomassa_nol PASSED [100%]
============================== 2 passed ==============================
kode keluar: 0
Skrip ini menjalankan pytest lewat subprocess pada folder contoh_sederhana/. Pada keluaran, test_hitung.py memuat dua fungsi tes, keduanya PASSED (lulus), dan ringkasannya “2 passed”. Kode keluar 0 artinya semuanya beres. pytest memberi kode keluar 1 bila ada tes gagal, sehingga bisa dipakai oleh skrip otomatis dan CI (Bab 20). Pada keluaran buku ini, waktu proses di ringkasan sengaja dibuang supaya hasilnya tetap sama setiap kali dibuat.
Saat tes gagal
Tes yang selalu lulus belum membuktikan apa-apa; Anda perlu tahu seperti apa laporannya bila gagal. Berkas contoh_gagal/test_gagal.py sengaja berisi harapan yang salah (400 kg, padahal hitungannya 432,4 kg).
04_tes_gagal.py
# Bab 18 - Membaca laporan tes yang gagal
from bantu import FOLDER, jalankan_pytest
teks, kode = jalankan_pytest(["-q"], FOLDER / "contoh_gagal")
print(teks)
print("kode keluar:", kode)F [100%]
================================== FAILURES ===================================
_________________________ test_biomassa_salah_harapan _________________________
def test_biomassa_salah_harapan():
ikan = Ikan("lele", 800, 540.5, 7.2)
> assert ikan.biomassa_kg() == 400.0
E AssertionError: assert 432.4 == 400.0
E + where 432.4 = biomassa_kg()
E + where biomassa_kg = Ikan(jenis='lele', jumlah=800, bobot_rata_g=540.5, ph_air=7.2).biomassa_kg
test_gagal.py:6: AssertionError
=========================== short test summary info ===========================
FAILED test_gagal.py::test_biomassa_salah_harapan - AssertionError: assert 43...
1 failed
kode keluar: 1
Cara membacanya dari atas:
- Huruf
Fmenandai satu tes gagal (tes lulus berupa titik). - Tanda panah
>menunjuk barisassertyang gagal. - Baris
E(error) menjelaskan: nilai sebenarnya 432,4, harapannya 400,0. pytest bahkan menguraikan dari mana 432,4 berasal (biomassa_kg()pada objekIkanitu). - Terakhir ada ringkasan
FAILED ...dan kode keluar1.
Saat tes gagal, jangan langsung mengubah tesnya agar lulus. Tanyakan dulu: yang salah kodenya atau harapannya? Di contoh ini yang salah adalah harapannya. Kadang yang salah justru kodenya, dan itulah gunanya tes.
Menata folder tes
Paket inventaris dari Bab 16 sekarang jadi folder proyek/. Tes ditaruh di folder tes/ di samping paketnya, tidak dicampur. pytest menyarankan pemisahan seperti ini (dokumentasi resmi, 2026-10-10).
proyek/
pyproject.toml
inventaris/
__init__.py unit.py barang.py contoh.py laporan.py
tes/
conftest.py
test_barang.py test_unit.py test_laporan.py
Aturan pytest ditulis di pyproject.toml (Bab 17):
[tool.pytest.ini_options]
testpaths = ["tes"]
pythonpath = ["."]testpaths memberi tahu pytest untuk mencari tes hanya di tes/. pythonpath = ["."] menambahkan folder proyek ke jalur impor, sehingga import inventaris berhasil tanpa memasangnya dulu.
03_daftar_tes.py
# Bab 18 - Daftar tes yang ditemukan pytest di proyek (tanpa menjalankannya)
from bantu import FOLDER, jalankan_pytest
teks, kode = jalankan_pytest(["--collect-only", "-q"], FOLDER / "proyek")
print(teks)tes/test_barang.py::test_biomassa_ikan
tes/test_barang.py::test_kategori_pohon[28.0-kecil]
tes/test_barang.py::test_kategori_pohon[32.5-sedang]
tes/test_barang.py::test_kategori_pohon[45.2-besar]
tes/test_barang.py::test_kategori_pohon[40.0-besar]
tes/test_barang.py::test_jumlah_ikan_negatif_ditolak
tes/test_laporan.py::test_simpan_ringkasan
tes/test_unit.py::test_luas_nol_ditolak
tes/test_unit.py::test_induk_tidak_bisa_menghitung
tes/test_unit.py::test_hasil_kolam
tes/test_unit.py::test_ringkasan_kolam
tes/test_unit.py::test_ringkasan_lima_unit
tes/test_unit.py::test_isi_tidak_bisa_diubah_dari_luar
13 tests collected
Ini mode --collect-only: pytest hanya mengumpulkan tes tanpa menjalankannya. Dari tiga berkas, ditemukan 13 tes. Tes test_kategori_pohon muncul empat kali, dengan tanda [28.0-kecil] dan seterusnya. Sebabnya ada di bagian berikut.
Fixture, parametrize, dan raises
Tiga alat pytest yang paling sering dipakai, semuanya ada di proyek kita.
Fixture adalah “bahan siap pakai”: fungsi bertanda @pytest.fixture yang dibuat sekali dan dipinjamkan ke tes lewat nama. Letakkan di conftest.py agar semua berkas tes bisa memakainya.
conftest.py
"""Fixture bersama: bahan siap pakai untuk semua berkas tes."""
import pytest
from inventaris import Ikan, Kolam
from inventaris.contoh import buat_contoh
@pytest.fixture
def unit_contoh():
"""Lima unit contoh, dibuat baru untuk setiap tes."""
return buat_contoh()
@pytest.fixture
def kolam_a():
kolam = Kolam("A", 0.04)
kolam.tambah(Ikan("lele", 800, 540.5, 7.2))
return kolamTes cukup menulis nama fixture sebagai parameter, dan pytest mengisinya. Setiap tes mendapat objek baru, sehingga tes tidak saling mengganggu.
test_barang.py
import pytest
from inventaris import Ikan, Pohon
def test_biomassa_ikan():
ikan = Ikan("lele", 800, 540.5, 7.2)
assert ikan.biomassa_kg() == pytest.approx(432.4)
@pytest.mark.parametrize(
("diameter", "harapan"),
[(28.0, "kecil"), (32.5, "sedang"), (45.2, "besar"), (40.0, "besar")],
)
def test_kategori_pohon(diameter, harapan):
assert Pohon("Jati", diameter, 15.0).kategori() == harapan
def test_jumlah_ikan_negatif_ditolak():
with pytest.raises(ValueError, match="negatif"):
Ikan("lele", -1, 500.0, 7.0)@pytest.mark.parametrizemenjalankan satu fungsi tes untuk beberapa pasang data. Empat pasangan diameter menjadi empat tes (itulah[28.0-kecil]tadi). Diameter 40,0 sengaja dimasukkan untuk menguji batas: kategori besar dimulai tepat di 40.pytest.approx(432.4)membandingkan bilangan desimal dengan toleransi kecil, supaya kesalahan pembulatan tidak menggagalkan tes (ingat Bab 6: bilangan desimal bisa sedikit meleset).pytest.raises(ValueError, match="negatif")menyatakan: “kode ini harus melempar error”. Bila tidak melempar, tes gagal. Inilah cara menguji bahwa penjaga data bekerja.
test_unit.py
import pytest
from inventaris import Kolam, Unit
def test_luas_nol_ditolak():
with pytest.raises(ValueError):
Kolam("X", 0)
def test_induk_tidak_bisa_menghitung():
with pytest.raises(NotImplementedError):
Unit("Polos", 1.0).hasil()
def test_hasil_kolam(kolam_a):
assert kolam_a.hasil() == pytest.approx(432.4)
def test_ringkasan_kolam(kolam_a):
assert kolam_a.ringkasan() == "Kolam A: 1 catatan, 432.4 kg biomassa"
def test_ringkasan_lima_unit(unit_contoh):
teks = [unit.ringkasan() for unit in unit_contoh]
assert teks[0] == "Plot hutan A: 3 catatan, 2.0 pohon sehat"
assert teks[2] == "Sawah Hilir: 1 catatan, 1250.0 kg (2500 kg/ha)"
assert len(teks) == 5
def test_isi_tidak_bisa_diubah_dari_luar(kolam_a):
assert isinstance(kolam_a.isi, tuple)
with pytest.raises(AttributeError):
kolam_a.isi.append("x")Tes test_ringkasan_lima_unit memakai fixture unit_contoh dan membandingkan teks ringkasan persis. Ada pula tes test_induk_tidak_bisa_menghitung untuk perilaku polimorfisme Bab 16.
test_laporan.py
from inventaris.laporan import simpan_ringkasan
def test_simpan_ringkasan(tmp_path, unit_contoh):
jalur = tmp_path / "ringkasan.txt"
jumlah = simpan_ringkasan(unit_contoh, jalur)
isi = jalur.read_text(encoding="utf-8").splitlines()
assert jumlah == 5
assert isi[3] == "Kolam A: 1 catatan, 432.4 kg biomassa"Fixture bawaan tmp_path memberi folder sementara yang dibuang sesudah tes, sehingga tes yang menulis berkas tidak mengotori komputer Anda.
Tes yang baik: kecil (satu hal per tes), mandiri (tidak bergantung pada tes lain), dan namanya menjelaskan harapan. test_luas_nol_ditolak langsung memberi tahu apa yang rusak bila ia gagal.
Menjalankan sebagian tes
Pada proyek besar, Anda sering hanya ingin menjalankan tes yang berkaitan dengan bagian yang sedang diubah. Opsi -k memilih tes berdasarkan potongan nama, dan -v (verbose) menampilkan tiap tes.
05_pilih_tes.py
# Bab 18 - Menjalankan sebagian tes: -k memilih berdasarkan nama
from bantu import FOLDER, jalankan_pytest
teks, kode = jalankan_pytest(["-v", "-k", "kolam"], FOLDER / "proyek")
print(teks)============================= test session starts =============================
testpaths: tes
collecting ... collected 13 items / 11 deselected / 2 selected
tes/test_unit.py::test_hasil_kolam PASSED [ 50%]
tes/test_unit.py::test_ringkasan_kolam PASSED [100%]
====================== 2 passed, 11 deselected =======================
Dari 13 tes yang terkumpul, 11 dikesampingkan (deselected) dan hanya 2 yang namanya memuat “kolam” dijalankan.
Tata letak yang disarankan pytest untuk proyek yang akan dipasang adalah src/ layout: paket di src/inventaris/ dan tes di tes/, lalu dipasang dengan pip install -e . (“editable”: perubahan kode langsung terbaca tanpa memasang ulang). Buku ini memakai tata letak datar (paket langsung di proyek/) dengan pythonpath = ["."] karena lebih sederhana untuk pemula. Dokumentasi pytest juga menyarankan --import-mode=importlib untuk proyek baru; opsi itu tidak dipakai di sini.
Proyek Bersambung I1: tes untuk paket inventaris
Sekarang seluruh tes proyek dijalankan sekaligus. Lihat kembali unit_contoh dari Bab 16: kelima unit itu kini dijaga oleh tes.
06_proyek_semua_tes.py
# Bab 18 - Proyek bersambung I1: seluruh tes paket inventaris
from bantu import FOLDER, jalankan_pytest
teks, kode = jalankan_pytest(["-v"], FOLDER / "proyek")
print(teks)
print("kode keluar:", kode)============================= test session starts =============================
testpaths: tes
collecting ... collected 13 items
tes/test_barang.py::test_biomassa_ikan PASSED [ 7%]
tes/test_barang.py::test_kategori_pohon[28.0-kecil] PASSED [ 15%]
tes/test_barang.py::test_kategori_pohon[32.5-sedang] PASSED [ 23%]
tes/test_barang.py::test_kategori_pohon[45.2-besar] PASSED [ 30%]
tes/test_barang.py::test_kategori_pohon[40.0-besar] PASSED [ 38%]
tes/test_barang.py::test_jumlah_ikan_negatif_ditolak PASSED [ 46%]
tes/test_laporan.py::test_simpan_ringkasan PASSED [ 53%]
tes/test_unit.py::test_luas_nol_ditolak PASSED [ 61%]
tes/test_unit.py::test_induk_tidak_bisa_menghitung PASSED [ 69%]
tes/test_unit.py::test_hasil_kolam PASSED [ 76%]
tes/test_unit.py::test_ringkasan_kolam PASSED [ 84%]
tes/test_unit.py::test_ringkasan_lima_unit PASSED [ 92%]
tes/test_unit.py::test_isi_tidak_bisa_diubah_dari_luar PASSED [100%]
============================= 13 passed ==============================
kode keluar: 0
Semua 13 tes lulus. Mulai sekarang, setiap kali Anda mengubah kelas Unit atau anak-anaknya, jalankan python -m pytest. Bila ada angka yang berubah tanpa sengaja, pytest memberi tahu persis di mana. Itulah arti kalimat “aman diubah” di penutup seri.
Latihan
- Pada
contoh_sederhana/test_hitung.py, tambahkan tes ketiga:biomassa_kg(1250, 480)harus600. Jalankan denganpython -m pytestdi folder itu. - Ubah angka harapan pada
contoh_gagal/test_gagal.pymenjadipytest.approx(432.4)lalu jalankan; pastikan tes lulus. - Tulis tes
test_hasil_kandanguntuk kelasKandang: tambahkan tigaTernak(350, 412,5, dan 389,0 kg) dan periksa hasilnya 1151,5. - Tantangan: tulis tes berparameter untuk
Pohon.kategori()dengan lima diameter, termasuk batas 30,0. Lalu sengaja ubah>= 30menjadi> 30dibarang.py, jalankan tes, lihat tes batas gagal, dan kembalikan kodenya.
Ringkasan Bab
- Tes otomatis memeriksa kode Anda tiap kali dijalankan, jadi perubahan tidak merusak diam-diam.
assertadalah dasarnya; berkastest_*.pyberisi fungsitest_*dibaca otomatis oleh pytest.- Tes gagal ditandai
F; baris>menunjuk assert, barisEmemberi nilai sebenarnya dan harapannya. Kode keluar 1. - Fixture (
conftest.py),parametrize,pytest.approx,pytest.raises, dantmp_pathadalah alat sehari-hari. - Taruh tes di folder
tes/di samping paket; atur lewat[tool.pytest.ini_options]dipyproject.toml. - Paket
inventariskini punya 13 tes yang lulus.
Selanjutnya, di Bab 19: kualitas kode, cara merapikan gaya penulisan otomatis dengan ruff, mencatat jalannya program dengan logging, dan mendokumentasikan proyek.