Pytest Itu Apa? Cara Install, Menulis, dan Running Test di Python

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.

Kartu istilah pytest atau pustaka pengujian Python: library/framework Python untuk menulis dan menjalankan test otomatis, jadi kode dicek tanpa membaca print satu per satu

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 items berarti pytest menemukan tiga fungsi test.
  • ..F adalah hasil per test, sesuai urutan di file. Titik berarti lolos, F berarti gagal (failed).
  • Bagian FAILURES menunjukkan test mana yang gagal. Tanda > menunjuk baris yang gagal, dan baris E menjelaskan kenapa.
  • Baris - 6281234567890 adalah hasil yang kita harapkan, + 62081234567890 adalah 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_raises gagal dengan ValueError: Nomor HP tidak valid: 0812. Fungsinya bekerja dengan benar, tapi pytest menganggap error sebagai kegagalan.
  • test_dengan_raises lolos. Blok with 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_error gagal dengan Failed: 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:

  1. Fixture dengan scope lebih lebar dijalankan lebih dulu: session, lalu package, module, class, dan terakhir function.
  2. Di scope yang sama, fixture yang dibutuhkan fixture lain dijalankan lebih dulu. Contohnya daftar_kontak sebelum nomor_bersih.
  3. Di scope yang sama, fixture autouse dijalankan 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.py itu, sedalam apa pun.
  • Test di folder tetangga atau folder induk tidak bisa, dan akan mendapat error fixture not found.
  • Subfolder boleh punya conftest.py sendiri. Kalau ada fixture dengan nama yang sama, yang dipakai adalah fixture dari conftest.py yang paling dekat dengan file test-nya.
  • Kalau fixture harus berlaku di seluruh proyek, taruh conftest.py di 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 = tests memberi tahu pytest untuk mencari test di folder tests.
  • pythonpath = . menambahkan folder induk ke daftar tempat Python mencari modul, jadi nomor_hp bisa di-import.
  • addopts = -v membuat -v selalu aktif tanpa perlu diketik. Opsi yang diketik manual tetap ditambahkan, jadi pytest -s berjalan sebagai pytest -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:

  1. Tulis test-nya dulu dengan parametrize, dengan hasil yang diharapkan 6281234567890. Jalankan, dan pastikan test-nya gagal.
  2. Ubah normalisasi_nomor_hp supaya tanda kurung ikut dibuang.
  3. Jalankan lagi sampai lolos, dan pastikan test lama tetap lolos.
  4. Tambahkan test dengan pytest.raises untuk nomor berisi huruf, misalnya 0812-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

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