Hai, teman-teman! Coba bayangin kamu baru menulis fungsi kecil untuk merapikan nomor HP dari form pendaftaran. Supaya yakin fungsinya benar, kamu menambahkan tiga baris print(), menjalankan skripnya, lalu membaca hasilnya satu per satu. Semua kelihatan beres. Padahal ada satu bug yang lolos dari mata. Di artikel ini kita jawab pytest itu apa, lewat bug kecil tersebut.
TL;DR: pytest adalah library/framework testing pihak ketiga untuk Python. Kita menulis fungsi berawalan test_ berisi assert, lalu pytest menemukan dan menjalankan semuanya dalam satu perintah. Cara install-nya pip install -U pytest (versi 9.1.1 butuh Python 3.10 ke atas). Cara running-nya cukup ketik pytest. Hasilnya berupa titik untuk test yang lolos dan F untuk yang gagal, plus penjelasan bagian mana yang tidak cocok.
Masalahnya: Mengecek Kode dengan print()
Ini fungsi yang mau kita cek. Tugasnya mengubah nomor HP Indonesia ke format 62xxxxxxxxxx, format yang biasa dipakai untuk mengirim pesan WhatsApp atau SMS lewat API:
def normalisasi_nomor_hp(nomor):
"""Ubah nomor HP Indonesia ke format 62xxxxxxxxxx."""
angka = nomor.replace("-", "").replace(" ", "")
if angka.startswith("+"):
angka = angka[1:]
if angka.startswith("0"):
angka = "62" + angka
if not angka.isdigit() or not 11 <= len(angka) <= 14:
raise ValueError(f"Nomor HP tidak valid: {nomor}")
return angka
Buat satu folder baru, misalnya belajar-pytest, lalu simpan fungsi di atas sebagai nomor_hp.py di dalamnya. Semua file berikutnya disimpan di folder yang sama.
Cara mengecek yang paling cepat terpikir tentu saja print(). Buat file cek_manual.py:
from nomor_hp import normalisasi_nomor_hp
print(normalisasi_nomor_hp("+62 812 3456 7890"))
print(normalisasi_nomor_hp("6281234567890"))
print(normalisasi_nomor_hp("0812-3456-7890"))
Jalankan dengan python cek_manual.py. Hasilnya:
6281234567890
6281234567890
62081234567890
Coba lihat baris ketiga. Ada angka 0 yang nyelip setelah 62, karena kode di atas lupa membuang nol di depan. Kalau kamu langsung melihatnya, selamat. Masalahnya, print() hanya menampilkan hasil. Yang menilai benar atau salah tetap mata kita, dan mata gampang capek kalau barisnya sudah puluhan.
Yang sebenarnya kita butuhkan adalah cara menuliskan "hasil yang seharusnya" sekali saja, lalu membiarkan komputer yang membandingkan setiap kali kode berubah. Itulah pekerjaan pytest.
Pytest Itu Apa?
pytest adalah library/framework testing untuk Python yang dipakai untuk menulis dan menjalankan unit test. Unit test adalah kode kecil yang mengecek satu bagian program, misalnya satu fungsi, dengan membandingkan hasilnya ke hasil yang diharapkan. pytest bukan bawaan Python, jadi perlu di-install dulu. Dokumentasi lengkapnya ada di situs resmi pytest.
Python sebenarnya sudah punya modul testing bawaan, yaitu unittest. Bedanya, unittest mengharuskan test ditulis di dalam class dengan method seperti self.assertEqual(). Di pytest, test cukup berupa fungsi biasa dengan assert bawaan Python. Kabar baiknya, pytest juga bisa menjalankan test lama yang ditulis dengan unittest, jadi pindah ke pytest tidak harus menulis ulang semuanya.
Supaya kebayang bedanya, ini test yang sama kalau ditulis dengan unittest:
import unittest
from nomor_hp import normalisasi_nomor_hp
class TestNormalisasiNomorHp(unittest.TestCase):
def setUp(self):
self.nomor_target = "6281234567890"
def test_awalan_nol(self):
self.assertEqual(normalisasi_nomor_hp("0812-3456-7890"), self.nomor_target)
def test_nomor_terlalu_pendek(self):
with self.assertRaises(ValueError):
normalisasi_nomor_hp("0812")
if __name__ == "__main__":
unittest.main()
Test harus berada di dalam class turunan unittest.TestCase, setiap method memakai self, dan pengecekannya lewat method khusus seperti assertEqual. Data disiapkan di setUp, yang dijalankan sebelum setiap test. Versi pytest dari test yang sama akan kita tulis sebentar lagi, dan hasilnya jauh lebih pendek.
pytest punya dua peran sekaligus. Pertama, sebagai test runner yang mencari dan menjalankan semua test di proyek. Kedua, sebagai framework yang menyediakan alat bantu seperti fixture dan parametrize, yang akan kita bahas di bawah.
Analoginya seperti petugas QC di pabrik yang memegang checklist. Kita cukup menulis checklist-nya sekali. Setiap kali ada perubahan, petugas QC mencentang semua poin dan hanya melapor kalau ada yang tidak sesuai.
Cara Install pytest
Cara install pytest adalah dengan perintah pip install -U pytest, sebaiknya di dalam virtual environment supaya tidak bercampur dengan library proyek lain. Menurut halaman pytest di PyPI, versi terbaru saat artikel ini ditulis adalah 9.1.1, yang butuh Python 3.10 ke atas.
Kalau belum terbiasa dengan virtual environment, ini langkah lengkapnya, mengikuti panduan Get Started pytest. Penjelasan soal virtual environment ada di dokumentasi modul venv.
python -m venv venv
venv\Scripts\activate
pip install -U pytest
Perintah kedua di atas untuk Windows. Di Linux atau macOS, ganti dengan source venv/bin/activate. Setelah itu, cek apakah pytest sudah terpasang:
pytest --version
Kalau berhasil, muncul versinya:
pytest 9.1.1
Semua contoh di artikel ini diuji dengan pytest 9.1.1 dan Python 3.14.
Test Pertama: Aturan Nama File dan Fungsi
Supaya ditemukan pytest, nama file test harus berawalan test_ (atau berakhiran _test.py), dan nama fungsi test harus berawalan test. Isinya cukup assert yang membandingkan hasil fungsi dengan hasil yang diharapkan. Aturan ini tertulis di panduan Good Integration Practices.
Kita ubah tiga print() tadi jadi tiga test. Simpan di file test_nomor_hp.py, satu folder dengan nomor_hp.py:
from nomor_hp import normalisasi_nomor_hp
def test_format_internasional():
assert normalisasi_nomor_hp("+62 812 3456 7890") == "6281234567890"
def test_sudah_format_62():
assert normalisasi_nomor_hp("6281234567890") == "6281234567890"
def test_awalan_nol():
assert normalisasi_nomor_hp("0812-3456-7890") == "6281234567890"
Perhatikan bedanya dengan versi print(). Sekarang setiap baris memuat dua hal: apa yang dijalankan, dan apa hasil yang seharusnya. Keputusan benar atau salah tidak lagi bergantung pada mata kita.
Satu hal yang perlu diingat: pytest menganggap test lolos selama tidak ada error. Fungsi test yang kosong (pass) atau yang isinya cuma print() akan selalu PASSED, padahal tidak mengecek apa pun. Bahkan return False pun tetap lolos, pytest hanya memberi peringatan Did you mean to use assert instead of return?. Jadi pastikan setiap test punya assert.
Aturan nama ini terlihat sepele, tapi sering jadi jebakan pertama. Waktu mencoba, saya sempat memberi nama cek_nomor_hp.py. pytest tidak protes sama sekali, dia hanya bilang tidak ada test yang ditemukan:
collected 0 items
============================ no tests ran in 0.03s ============================
Jadi kalau muncul collected 0 items, cek dulu nama file dan nama fungsinya.
Cara Running pytest dan Membaca Hasilnya
Sekarang bagian yang paling ditunggu. Isi folder belajar-pytest saat ini seharusnya seperti ini:
belajar-pytest/
├── cek_manual.py
├── nomor_hp.py
└── test_nomor_hp.py
Buka terminal, masuk ke folder itu dengan cd belajar-pytest, pastikan virtual environment sudah aktif, lalu ketik:
pytest
Hasilnya:
============================= test session starts =============================
platform win32 -- Python 3.14.0, pytest-9.1.1, pluggy-1.7.0
rootdir: C:\belajar-pytest\2_test_pertama
collected 3 items
test_nomor_hp.py ..F [100%]
================================== FAILURES ===================================
_______________________________ test_awalan_nol _______________________________
def test_awalan_nol():
> assert normalisasi_nomor_hp("0812-3456-7890") == "6281234567890"
E AssertionError: assert '62081234567890' == '6281234567890'
E
E - 6281234567890
E + 62081234567890
E ? +
test_nomor_hp.py:13: AssertionError
=========================== short test summary info ===========================
FAILED test_nomor_hp.py::test_awalan_nol - AssertionError: assert '6208123456...
========================= 1 failed, 2 passed in 0.08s =========================
Kita baca pelan-pelan dari atas:
collected 3 itemsberarti pytest menemukan tiga fungsi test...Fadalah hasil per test, sesuai urutan di file. Titik berarti lolos,Fberarti gagal (failed).- Bagian
FAILURESmenunjukkan test mana yang gagal. Tanda>menunjuk baris yang gagal, dan barisEmenjelaskan kenapa. - Baris
- 6281234567890adalah hasil yang kita harapkan,+ 62081234567890adalah hasil sebenarnya, dan tanda? +menunjuk karakter yang berlebih. Itu dia, nol yang tadi nyelip. - Baris terakhir adalah ringkasan: 1 gagal, 2 lolos.
Fitur pembanding ini yang membuat assert biasa di pytest terasa jauh lebih berguna daripada assert di skrip biasa. Detailnya dijelaskan di dokumentasi assertion pytest.
Bug-nya sekarang jelas: nol di depan harus dibuang dulu sebelum ditambah 62. Perbaikannya cuma satu baris:
if angka.startswith("0"):
angka = "62" + angka[1:]
Setelah diperbaiki, jalankan pytest lagi dan ketiga test lolos. Tanpa perlu membaca ulang satu per satu.
Opsi Running yang Sering Dipakai
Beberapa opsi yang paling sering saya pakai, semuanya ada di panduan menjalankan pytest:
| Perintah | Fungsinya |
|---|---|
pytest -v |
Menampilkan nama setiap test beserta status PASSED atau FAILED |
pytest -q |
Kebalikan -v, tampilan lebih ringkas |
pytest -k awalan |
Hanya menjalankan test yang namanya mengandung kata awalan |
pytest -x |
Berhenti di test pertama yang gagal |
pytest -s |
Menampilkan hasil print() di dalam test, yang biasanya disembunyikan pytest |
pytest test_nomor_hp.py |
Hanya menjalankan satu file |
Contohnya dengan -v, setiap test tampil di barisnya sendiri:
test_nomor_hp.py::test_format_internasional PASSED [ 33%]
test_nomor_hp.py::test_sudah_format_62 PASSED [ 66%]
test_nomor_hp.py::test_awalan_nol FAILED [100%]
Opsi -k berguna saat sedang fokus memperbaiki satu test. Hasilnya collected 3 items / 2 deselected / 1 selected, jadi dua test lain dilewati.
Menguji Error dengan pytest.raises
Untuk mengecek bahwa fungsi memang menolak input yang salah, pakai with pytest.raises(NamaError):. Test lolos kalau error itu muncul di dalam blok with, dan gagal kalau tidak muncul. Opsi match bisa ditambahkan untuk mengecek isi pesan error-nya.
Test sejauh ini mengecek bahwa input yang benar menghasilkan output yang benar. Tapi fungsi yang baik juga harus menolak input yang salah. Nomor 0812 terlalu pendek, jadi fungsi kita seharusnya melempar ValueError. Masalahnya, error di dalam test justru membuat test gagal.
Coba bandingkan tiga test ini:
import pytest
from nomor_hp import normalisasi_nomor_hp
def test_tanpa_raises():
normalisasi_nomor_hp("0812")
def test_dengan_raises():
with pytest.raises(ValueError, match="tidak valid"):
normalisasi_nomor_hp("0812")
def test_raises_tapi_tidak_error():
with pytest.raises(ValueError):
normalisasi_nomor_hp("0812-3456-7890")
Hasilnya F.F:
test_tanpa_raisesgagal denganValueError: Nomor HP tidak valid: 0812. Fungsinya bekerja dengan benar, tapi pytest menganggap error sebagai kegagalan.test_dengan_raiseslolos. Blokwith pytest.raises(ValueError)memberi tahu pytest bahwa di sini error itu justru yang diharapkan.match="tidak valid"sekaligus mengecek isi pesannya.test_raises_tapi_tidak_errorgagal denganFailed: DID NOT RAISE ValueError. Nomornya valid, jadi error tidak muncul. Kalau ini terjadi di kasus nyata, artinya fungsi kita terlalu longgar dan meloloskan input yang seharusnya ditolak.
Perhatikan bahwa sekarang kita perlu import pytest, karena pytest.raises berasal dari library pytest. Test sebelumnya tidak butuh import ini karena hanya memakai assert.
Satu Test untuk Banyak Data: parametrize
@pytest.mark.parametrize menjalankan satu fungsi test berkali-kali dengan data yang berbeda. Kita menulis daftar pasangan input dan hasil yang diharapkan, dan pytest membuat satu test terpisah untuk setiap pasangan. Kodenya lebih pendek, dan laporan gagalnya tetap jelas per data.
Setelah tiga test tadi, saya ingin menambah format lain: nomor dengan spasi, nomor 10 digit, nomor operator lain. Kalau setiap format jadi fungsi baru, file test akan penuh fungsi kembar yang isinya cuma beda angka. Mirip mengisi formulir yang sama berkali-kali, padahal yang beda cuma satu kolom.
Di sinilah parametrize berguna:
import pytest
from nomor_hp import normalisasi_nomor_hp
@pytest.mark.parametrize(
"nomor, hasil",
[
("+62 812 3456 7890", "6281234567890"),
("6281234567890", "6281234567890"),
("0812-3456-7890", "6281234567890"),
("0812 3456 7890", "6281234567890"),
("0812345678", "62812345678"),
("+6285712345678", "6285712345678"),
],
)
def test_normalisasi(nomor, hasil):
assert normalisasi_nomor_hp(nomor) == hasil
@pytest.mark.parametrize("nomor", ["0812", "", "0812-3456-7890-1234"])
def test_nomor_tidak_valid(nomor):
with pytest.raises(ValueError):
normalisasi_nomor_hp(nomor)
String "nomor, hasil" adalah nama parameter, dan nama itu harus sama persis dengan parameter fungsi test di bawahnya. Hasil pytest -v:
test_2_parametrize.py::test_normalisasi[+62 812 3456 7890-6281234567890] PASSED [ 11%]
test_2_parametrize.py::test_normalisasi[6281234567890-6281234567890] PASSED [ 22%]
test_2_parametrize.py::test_normalisasi[0812-3456-7890-6281234567890] PASSED [ 33%]
test_2_parametrize.py::test_normalisasi[0812 3456 7890-6281234567890] PASSED [ 44%]
test_2_parametrize.py::test_normalisasi[0812345678-62812345678] PASSED [ 55%]
test_2_parametrize.py::test_normalisasi[+6285712345678-6285712345678] PASSED [ 66%]
test_2_parametrize.py::test_nomor_tidak_valid[0812] PASSED [ 77%]
test_2_parametrize.py::test_nomor_tidak_valid[] PASSED [ 88%]
test_2_parametrize.py::test_nomor_tidak_valid[0812-3456-7890-1234] PASSED [100%]
============================== 9 passed in 0.04s ==============================
Dua fungsi menghasilkan sembilan test. Data setiap test ditulis di dalam kurung siku, jadi kalau ada yang gagal, kita langsung tahu data mana penyebabnya.
Jumlah parameternya tidak harus dua. Tulis saja "nama, nomor, umur" dengan tuple berisi tiga nilai. Syaratnya, jumlah isi tuple sama dengan jumlah nama parameter. Data bertingkat juga bisa, misalnya satu parameter kontak berisi dict yang di dalamnya ada list nomor. Untuk data seperti itu, tambahkan ids supaya nama test-nya mudah dibaca:
@pytest.mark.parametrize(
"kontak",
[
{"nama": "Budi", "nomor": ["0812-3456-7890", "0857-1111-2222"]},
{"nama": "Sari", "nomor": ["+62 857 1234 5678"]},
],
ids=["budi-dua-nomor", "sari-satu-nomor"],
)
def test_data_nested(kontak):
assert len(kontak["nomor"]) >= 1
Hasilnya test_data_nested[budi-dua-nomor] dan test_data_nested[sari-satu-nomor]. Kalau dua @pytest.mark.parametrize ditumpuk di atas satu fungsi, pytest membuat semua kombinasinya, jadi 2 data kali 2 data menghasilkan 4 test.
Cerita kecil soal ini: di percobaan pertama saya menulis 081234567 sebagai contoh nomor terpendek, dan test-nya gagal. Ternyata itu cuma 9 digit, sedangkan aturan di fungsi minimal 11 digit setelah jadi format 62. Kodenya benar, data test-nya yang salah. Jadi kalau test gagal, jangan langsung menyalahkan kode, cek juga data test-nya.
Fixture: Menyiapkan Data Sebelum Test
Fixture adalah fungsi yang menyiapkan data atau object untuk test. Fungsinya diberi dekorator @pytest.fixture, lalu test cukup menulis nama fixture itu sebagai parameter. pytest akan menjalankan fixture-nya lebih dulu dan mengirim hasilnya ke test, tanpa kita memanggilnya sendiri.
Sekarang skenarionya naik sedikit. Kita punya daftar kontak, dan beberapa test butuh daftar yang sama. Menyalin daftar itu ke setiap test jelas melelahkan. Tapi sebelum memakai fixture, wajar kalau muncul pertanyaan: kenapa tidak pakai variabel global atau fungsi biasa saja?
Kenapa Tidak Pakai Variabel Global atau Fungsi Biasa?
Kita coba dulu cara yang paling cepat, yaitu variabel global:
DAFTAR_KONTAK = ["0812-3456-7890", "+62 857 1234 5678"]
def test_tambah_kontak():
DAFTAR_KONTAK.append("0813-0000-1111")
assert len(DAFTAR_KONTAK) == 3
def test_jumlah_kontak():
assert len(DAFTAR_KONTAK) == 2
Test kedua gagal dengan assert 3 == 2, karena list-nya sudah diubah oleh test pertama. Anehnya, kalau test_jumlah_kontak dijalankan sendirian, test itu lolos. Test yang hasilnya bergantung pada urutan seperti ini paling susah dicari penyebabnya.
Masalah itu bisa diatasi dengan fungsi biasa yang membuat data baru setiap kali dipanggil. Untuk data sederhana, cara ini sebenarnya sudah cukup. Masalah baru muncul saat persiapannya butuh bersih-bersih, misalnya membuat file sementara:
from pathlib import Path
def siapkan_file_kontak():
path = Path("kontak_fungsi.txt")
path.write_text("0812-3456-7890\n+62 857 1234 5678\n")
return path
def test_baca_kontak():
path = siapkan_file_kontak()
baris = path.read_text().splitlines()
assert len(baris) == 3
path.unlink()
assert di atas sengaja salah. Begitu gagal, baris path.unlink() tidak pernah dijalankan, dan kontak_fungsi.txt tertinggal di folder. Bayangkan kalau yang tertinggal adalah data di database test.
Bandingkan dengan fixture yang memakai yield:
from pathlib import Path
import pytest
@pytest.fixture
def file_kontak():
path = Path("kontak_fixture.txt")
path.write_text("0812-3456-7890\n+62 857 1234 5678\n")
yield path
path.unlink()
print("\n[fixture] file dihapus")
def test_baca_kontak(file_kontak):
baris = file_kontak.read_text().splitlines()
assert len(baris) == 3
Kode sebelum yield adalah persiapan, nilai setelah yield dikirim ke test, dan kode setelahnya adalah bersih-bersih. Test ini juga gagal, tapi pytest tetap menjalankan bagian setelah yield. Hasil pytest -s menampilkan [fixture] file dihapus, dan yang tersisa di folder hanya kontak_fungsi.txt dari cara sebelumnya.
Kalau dirangkum head to head:
| Variabel global | Fungsi biasa | Fixture | |
|---|---|---|---|
| Data baru untuk setiap test | Tidak, dipakai bersama | Ya | Ya (scope bawaan function) |
| Bersih-bersih walaupun test gagal | Tidak | Tidak, kecuali menulis try/finally sendiri |
Ya, lewat yield |
| Persiapan berat cukup dibuat sekali | Ya, tapi rawan ikut berubah | Tidak, dibuat ulang setiap dipanggil | Ya, lewat scope |
| Dipakai banyak file test tanpa import | Tidak | Tidak | Ya, lewat conftest.py |
| Cara memakai | Langsung dipakai | Dipanggil manual di setiap test | Cukup tulis namanya sebagai parameter |
Jadi fixture bukan sekadar soal kode yang rapi. Fixture menjamin setiap test mulai dari kondisi yang sama dan meninggalkan folder tetap bersih. Library lain juga menyediakan fitur lewat fixture, seperti fixture faker yang akan kita pakai di bagian 6.
Fixture untuk Daftar Kontak
Kembali ke daftar kontak. Fixture untuk kasus ini mirip meja prasmanan: disiapkan sekali, dan setiap tamu tinggal mengambil:
import pytest
from nomor_hp import normalisasi_nomor_hp
@pytest.fixture(scope="session")
def daftar_kontak():
print("\n[fixture] menyiapkan daftar kontak")
return [
{"nama": "Budi", "nomor": "0812-3456-7890"},
{"nama": "Sari", "nomor": "+62 857 1234 5678"},
{"nama": "Budi (kantor)", "nomor": "+62 812 3456 7890"},
]
@pytest.fixture
def nomor_bersih(daftar_kontak):
return [normalisasi_nomor_hp(k["nomor"]) for k in daftar_kontak]
@pytest.fixture(autouse=True)
def penanda_test():
print("\n[autouse] test dimulai")
Lalu test-nya:
def test_jumlah_kontak(daftar_kontak):
assert len(daftar_kontak) == 3
def test_semua_nomor_format_62(nomor_bersih):
assert all(nomor.startswith("62") for nomor in nomor_bersih)
def test_nomor_kembar_ketahuan(nomor_bersih):
assert len(set(nomor_bersih)) == 2
Test terakhir menunjukkan kenapa normalisasi penting. "Budi" dan "Budi (kantor)" ditulis dengan format berbeda, tapi setelah dinormalisasi ternyata nomornya sama. Jadi dari tiga kontak, hanya ada dua nomor unik.
Ada tiga hal baru di fixture tadi.
Fixture boleh memakai fixture lain. nomor_bersih meminta daftar_kontak sebagai parameter, sama seperti test. Saat sebuah test meminta nomor_bersih, pytest melihat bahwa nomor_bersih butuh daftar_kontak. Jadi pytest menjalankan daftar_kontak dulu, hasilnya dikirim ke nomor_bersih, lalu hasil nomor_bersih dikirim ke test. Pola ini akan muncul lagi saat fixture dipakai bersama library lain.
scope menentukan seberapa sering fixture dibuat ulang. Ada lima pilihan, sesuai panduan fixture pytest:
| Scope | Fixture dibuat ulang setiap |
|---|---|
function (bawaan) |
satu test |
class |
satu class test, misalnya class TestKontak: |
module |
satu file test |
package |
satu folder test |
session |
sekali untuk seluruh test dalam satu kali menjalankan pytest |
Hati-hati dengan scope yang lebih lebar dari function. Semua test di dalam scope itu memakai object yang sama, jadi kalau ada test yang mengubah isinya, perubahan itu terbawa ke test berikutnya, persis seperti variabel global tadi. Pegangannya: data yang mungkin diubah test pakai scope function, sedangkan persiapan berat yang hanya dibaca, seperti membaca file besar, boleh pakai session. Di contoh ini daftar_kontak aman memakai session karena ketiga test hanya membacanya.
Kalau kamu datang dari unittest, scope ini padanan setUp (scope function), setUpClass (scope class), dan setUpModule (scope module). Bedanya, di pytest setiap test memilih sendiri fixture yang dibutuhkan lewat parameter.
autouse=True membuat fixture berjalan otomatis di setiap test, walaupun tidak diminta lewat parameter. Contoh di atas hanya mencetak penanda supaya terlihat, tapi di dunia nyata autouse biasa dipakai untuk menyiapkan pengaturan yang harus berlaku di semua test. Kalau autouse tidak ditulis, nilainya False, dan fixture hanya dijalankan untuk test yang menyebut namanya sebagai parameter.
Buktinya terlihat kalau dijalankan dengan pytest -s. Secara bawaan pytest menyembunyikan hasil print() dari test yang lolos, dan -s mematikan perilaku itu:
tests/test_kontak.py::test_jumlah_kontak
[fixture] menyiapkan daftar kontak
[autouse] test dimulai
PASSED
tests/test_kontak.py::test_semua_nomor_format_62
[autouse] test dimulai
PASSED
tests/test_kontak.py::test_nomor_kembar_ketahuan
[autouse] test dimulai
PASSED
============================== 3 passed in 0.04s ==============================
Penanda [autouse] muncul tiga kali, sekali per test. Sementara [fixture] menyiapkan daftar kontak hanya muncul sekali karena scope-nya session. Waktu saya mencoba menghapus scope="session", baris itu muncul tiga kali. Untuk data kecil seperti ini bedanya tidak terasa, tapi untuk fixture yang berat, misalnya membaca file besar, scope bisa menghemat banyak waktu.
Urutan Fixture: Siapa yang Jalan Duluan?
Ada yang menarik saat scope="session" dihapus. Bukan cuma jumlahnya yang berubah, urutannya juga ikut terbalik:
tests/test_kontak.py::test_jumlah_kontak
[autouse] test dimulai
[fixture] menyiapkan daftar kontak
PASSED
tests/test_kontak.py::test_semua_nomor_format_62
[autouse] test dimulai
[fixture] menyiapkan daftar kontak
PASSED
Sebelumnya [fixture] muncul duluan, sekarang [autouse] yang duluan. Ini bukan kebetulan. Menurut aturan urutan fixture di dokumentasi pytest, urutannya ditentukan tiga aturan:
- Fixture dengan scope lebih lebar dijalankan lebih dulu:
session, lalupackage,module,class, dan terakhirfunction. - Di scope yang sama, fixture yang dibutuhkan fixture lain dijalankan lebih dulu. Contohnya
daftar_kontaksebelumnomor_bersih. - Di scope yang sama, fixture
autousedijalankan lebih dulu daripada fixture yang diminta lewat parameter.
Saat daftar_kontak memakai scope session, aturan pertama yang berlaku, karena session lebih lebar dari function milik penanda_test. Begitu keduanya sama-sama function, aturan pertama tidak membedakan lagi, dan aturan ketiga membuat autouse jalan duluan.
Autouse didahulukan karena biasanya dipakai untuk menyiapkan lingkungan yang harus sudah siap sebelum data lain dibuat, misalnya seed, locale, atau pengaturan library. Prinsip ini yang nanti dipakai di bagian 6 untuk mengatur locale Faker.
conftest.py: Fixture Bersama Tanpa Import
Kalau fixture ditulis di dalam satu file test, fixture itu hanya bisa dipakai di file tersebut. Supaya bisa dipakai banyak file test, tulis fixture di file khusus bernama conftest.py. Fixture di dalamnya otomatis tersedia untuk semua test di folder yang sama dan subfoldernya, tanpa import.
Itulah kenapa ketiga fixture di atas saya simpan di tests/conftest.py, sementara test-nya ada di tests/test_kontak.py. File test tidak punya satu baris import pun, tapi tetap bisa memakai daftar_kontak dan nomor_bersih.
Jangkauan conftest.py mengikuti folder:
- Test di folder yang sama dan di semua subfoldernya bisa memakai fixture dari
conftest.pyitu, sedalam apa pun. - Test di folder tetangga atau folder induk tidak bisa, dan akan mendapat error
fixture not found. - Subfolder boleh punya
conftest.pysendiri. Kalau ada fixture dengan nama yang sama, yang dipakai adalah fixture dariconftest.pyyang paling dekat dengan file test-nya. - Kalau fixture harus berlaku di seluruh proyek, taruh
conftest.pydi folder paling atas.
pytest.ini: Pengaturan Sekali Tulis
Setelah test dipindah ke folder tests/, muncul masalah baru. Fungsi normalisasi_nomor_hp ada di nomor_hp.py di folder induk, dan pytest tidak bisa menemukannya:
ImportError while loading conftest '...\tests\conftest.py'.
tests\conftest.py:3: in <module>
from nomor_hp import normalisasi_nomor_hp
E ModuleNotFoundError: No module named 'nomor_hp'
Solusinya file pytest.ini di folder induk. File ini berisi pengaturan yang dibaca pytest setiap kali dijalankan, sesuai dokumentasi konfigurasi pytest:
[pytest]
testpaths = tests
pythonpath = .
addopts = -v
testpaths = testsmemberi tahu pytest untuk mencari test di foldertests.pythonpath = .menambahkan folder induk ke daftar tempat Python mencari modul, jadinomor_hpbisa di-import.addopts = -vmembuat-vselalu aktif tanpa perlu diketik. Opsi yang diketik manual tetap ditambahkan, jadipytest -sberjalan sebagaipytest -v -s.
Letak pytest.ini juga menentukan rootdir, yaitu folder acuan proyek. pythonpath = . berarti folder tempat pytest.ini berada. pytest mencari file ini sampai ke folder-folder di atasnya, jadi walaupun pytest dijalankan dari dalam folder tests, output tetap menampilkan configfile: pytest.ini dan pengaturannya tetap berlaku.
Struktur foldernya jadi seperti ini:
4_fixture/
├── nomor_hp.py
├── pytest.ini
└── tests/
├── conftest.py
└── test_kontak.py
Error yang Sering Muncul di Awal
Ini kumpulan error yang paling sering saya temui waktu mulai belajar pytest, beserta penyebabnya:
| Pesan | Penyebab | Solusi |
|---|---|---|
'pytest' is not recognized as an internal or external command |
pytest belum terpasang, atau virtual environment belum aktif | Aktifkan venv, atau jalankan dengan python -m pytest |
collected 0 items |
Nama file atau fungsi tidak berawalan test_ |
Ganti nama, misalnya cek_nomor_hp.py jadi test_nomor_hp.py |
ModuleNotFoundError |
Test di subfolder tidak bisa menemukan modul di folder induk | Tambahkan pythonpath = . di pytest.ini |
fixture 'daftar_kontk' not found |
Salah ketik nama fixture, atau fixture tidak ada di conftest.py yang terjangkau |
Cek ejaan, pytest juga menampilkan daftar fixture yang tersedia |
Untuk error terakhir, pytest cukup membantu. Di bawah pesan error-nya ada baris available fixtures: yang memuat daftar_kontak, jadi salah ketiknya langsung kelihatan.
Tantangan untuk Kamu
Sekarang giliranmu. Ada orang yang menulis nomornya dengan tanda kurung, seperti (0812) 3456-7890 atau +62 (812) 3456 7890. Fungsi kita saat ini menolaknya.
Coba dengan urutan ini:
- Tulis test-nya dulu dengan
parametrize, dengan hasil yang diharapkan6281234567890. Jalankan, dan pastikan test-nya gagal. - Ubah
normalisasi_nomor_hpsupaya tanda kurung ikut dibuang. - Jalankan lagi sampai lolos, dan pastikan test lama tetap lolos.
- Tambahkan test dengan
pytest.raisesuntuk nomor berisi huruf, misalnya0812-34S6-7890.
Kalau langkah 1 langsung lolos tanpa mengubah kode, ada yang salah dengan test-mu. Jawaban saya ada di folder 5_tantangan di GitHub.
Penutup
Jadi, pytest itu apa? Buat saya, pytest adalah cara memindahkan pekerjaan "membaca hasil print() dengan teliti" dari mata ke komputer. Bug nol yang nyelip di awal artikel ini sebenarnya kelihatan di layar. Tapi baru pytest yang benar-benar menunjuk karakter mana yang salah.
Kalau mau mulai di proyekmu sendiri, cari satu fungsi kecil yang hasilnya bisa ditebak, misalnya fungsi format tanggal, hitung diskon, atau validasi input. Tulis tiga test: satu kasus normal, satu kasus pinggir, dan satu input yang harus ditolak. Itu sudah cukup untuk merasakan bedanya.
Fixture yang kita bahas di sini akan dipakai lagi di Faker Python Series 6. Di sana, library Faker menyediakan fixture bernama faker yang langsung bisa dipakai sebagai parameter test, sama seperti daftar_kontak di atas. Kalau belum kenal Faker, mulai dari pengenalan library Faker dan membangun dataset dummy untuk testing. Bagian 6 juga memakai provider NIK dummy dari bagian 5. Artikel Python lainnya bisa dilihat di label Python.
Ada error pytest yang belum masuk tabel di atas? Tulis pesan error-nya di kolom komentar, nanti saya bantu cari penyebabnya.
Source code lengkap artikel ini bisa dilihat di GitHub: ApaItuPytest
FAQ
Pytest itu apa? pytest adalah library/framework testing pihak ketiga untuk Python yang dipakai untuk menulis dan menjalankan unit test. Test ditulis sebagai fungsi biasa berawalan test_ dengan assert, lalu pytest menemukan dan menjalankan semuanya dengan satu perintah. Kalau ada yang gagal, pytest menunjukkan nilai yang diharapkan dan nilai sebenarnya.
Apa bedanya pytest dan unittest? unittest adalah modul bawaan Python yang mengharuskan test ditulis di dalam class dengan method seperti self.assertEqual. pytest harus di-install terpisah, tapi test cukup berupa fungsi dengan assert biasa dan punya fitur tambahan seperti fixture dan parametrize. pytest juga bisa menjalankan test lama yang ditulis dengan unittest.
Bagaimana cara install pytest? Jalankan pip install -U pytest, sebaiknya di dalam virtual environment yang sudah aktif. Setelah itu cek dengan pytest --version. Versi pytest 9.1.1 membutuhkan Python 3.10 atau yang lebih baru.
Bagaimana cara menjalankan satu test saja di pytest? Gunakan opsi -k diikuti potongan nama test, misalnya pytest -k awalan untuk menjalankan test yang namanya mengandung kata awalan. Bisa juga menyebut nama file, atau nama file diikuti dua titik dua dan nama fungsi, misalnya pytest test_nomor_hp.py::test_awalan_nol.
Kenapa pytest menampilkan collected 0 items? Biasanya karena nama file test tidak berawalan test_ atau tidak berakhiran _test.py, atau nama fungsinya tidak berawalan test. pytest melewati file seperti itu tanpa pesan error. Cek juga apakah perintah pytest dijalankan dari folder yang benar.
Referensi
- Situs resmi pytest, diakses 25 Oktober 2026
- pytest di PyPI, diakses 24 Oktober 2026
- pytest: Get Started, diakses 24 Oktober 2026
- pytest: Good Integration Practices, diakses 24 Oktober 2026
- pytest: How to invoke pytest, diakses 24 Oktober 2026
- pytest: How to write and report assertions in tests, diakses 24 Oktober 2026
- pytest: How to parametrize fixtures and test functions, diakses 24 Oktober 2026
- pytest: How to use fixtures, diakses 24 Oktober 2026
- pytest: Fixtures reference, Fixture instantiation order, diakses 25 Oktober 2026
- pytest: Configuration, diakses 24 Oktober 2026
- pytest: How to capture stdout/stderr output, diakses 24 Oktober 2026
- pytest: How to use unittest-based tests with pytest, diakses 24 Oktober 2026
- Dokumentasi Python: unittest, diakses 24 Oktober 2026
- Dokumentasi Python: venv, diakses 24 Oktober 2026

Posting Komentar untuk "Pytest Itu Apa? Cara Install, Menulis, dan Running Test di Python"