Fungsi numerik jarang gagal pada data yang rapi; ia gagal pada tepi-tepinya. Array kosong, semua elemen nan, penyebut yang hampir nol, atau input yang membuat hasil meluap (overflow) — semuanya jalur yang tidak dilalui contoh dokumentasi, tetapi pasti dilalui data nyata. Merancang pengujian operasi numerik berarti menyusun kasus-kasus tepi itu secara sadar, ditambah pemeriksaan properti matematis yang harus selalu dipertahankan.

Bab ini membangun rangkaian pengujian untuk satu fungsi contoh bernama rasio — pembagi dua array — mulai dari kasus batas, format test file untuk pytest, hingga pengujian gaya properti dengan data acak ber-seed. Asersinya memakai fungsi yang sudah kita kenal dari Chapter numpy.testing.

Fungsi yang Diuji dan Kasus Batasnya #

Fungsi contoh kita menghitung pembagian dua array secara aman: mengonversi masukan ke float64 dan meredam peringatan pembagian nol melalui np.errstate. Pengujian di bawah ditulis sebagai test file yang juga dapat dijalankan langsung — tiap fungsi test_* dipanggil di akhir script:

python
import numpy as np

def rasio(a, b):
    a = np.asarray(a, dtype=np.float64)
    b = np.asarray(b, dtype=np.float64)
    with np.errstate(divide="ignore", invalid="ignore", over="ignore"):
        return a / b

def test_kasus_biasa():
    hasil = rasio(np.array([10.0, 20.0]), np.array([2.0, 4.0]))
    np.testing.assert_array_equal(hasil, np.array([5.0, 5.0]))

def test_array_kosong():
    assert rasio(np.array([]), np.array([])).size == 0

def test_penyebut_nol():
    hasil = rasio(np.array([1.0, 0.0]), np.array([0.0, 0.0]))
    assert np.isinf(hasil[0]) and np.isnan(hasil[1])

def test_overflow():
    hasil = rasio(np.array([1e308]), np.array([1e-308]))
    assert np.isinf(hasil[0])

def test_nilai_nan():
    assert np.isnan(rasio(np.array([np.nan]), np.array([1.0]))[0])

for uji in (test_kasus_biasa, test_array_kosong, test_penyebut_nol,
            test_overflow, test_nilai_nan):
    uji()
    print(uji.__name__, "-> lulus")

Output yang Dihasilkan:

text
test_kasus_biasa -> lulus
test_array_kosong -> lulus
test_penyebut_nol -> lulus
test_overflow -> lulus
test_nilai_nan -> lulus

Mari kita bedah kasus ujinya satu per satu:

  1. test_kasus_biasa: jalur bahagia (happy path) tetap ditulis lebih dulu — kalau kasus normal saja tidak lulus, kasus tepi tidak berarti apa-apa.
  2. test_array_kosong: operasi pada array kosong mengembalikan array kosong, bukan error; kontrak ini perlu dikunci agar kode pemanggil bisa andalkannya.
  3. test_penyebut_nol: NumPy tidak menolak pembagian nol pada float; hasilnya inf untuk 1.0/0.0 dan nan untuk 0.0/0.0. Pengujian menetapkan perilaku itu sebagai kontrak eksplisit — pola nilai khusus ini dibahas pada Chapter NaN dan Infinity.
  4. test_overflow: 1e308 / 1e-308 secara matematis bernilai 1e616, melampaui batas float64 sekitar 1.8e308; hasilnya inf. Pengujian memastikan fungsi tidak mengubah nilai lain atau menghentikan program saat meluap.
  5. test_nilai_nan: nan yang masuk harus keluar sebagai nan, tanpa menular ke elemen lain.

Format Test File untuk pytest #

Saat proyek membesar, pemanggilan manual digantikan pytest. Isi test file-nya sama persis — yang berubah hanya cara menjalankannya. Ilustrasi berikut menunjukkan wujud test_pipa.py versi pytest, termasuk penggunaan pytest.raises untuk kasus yang diharapkan error:

python
# notest
# isi test_pipa.py — dijalankan dengan: python -m pytest test_pipa.py -v
import numpy as np
import pytest
from pipa import rasio

def test_kasus_biasa():
    hasil = rasio(np.array([10.0, 20.0]), np.array([2.0, 4.0]))
    np.testing.assert_array_equal(hasil, np.array([5.0, 5.0]))

def test_penyebut_nol():
    hasil = rasio(np.array([1.0]), np.array([0.0]))
    assert np.isinf(hasil[0])

def test_ukuran_tidak_cocok():
    with pytest.raises(ValueError):
        rasio(np.array([1.0, 2.0]), np.array([1.0, 2.0, 3.0]))

Tiga hal yang perlu diperhatikan dari format di atas:

  1. Penamaan: file diawali test_ dan fungsi diawali test_ — itulah konvensi penemuan pytest; perintah python -m pytest test_pipa.py -v akan menjalankan ketiganya dan melaporkan jumlah yang lulus.
  2. pytest.raises: context manager ini padanan np.testing.assert_raises; pembagian array (2,) dengan (3,) memang memunculkan ValueError broadcasting, dan pengujian gagal bila exception itu tidak datang.
  3. Satu gagasan per fungsi: setiap fungsi menguji satu perilaku dengan satu asersi inti, sehingga nama fungsi menjadi dokumentasi kasus yang dilindungi.

Pengujian Gaya Properti dengan Generator Ber-seed #

Kasus batas menguji titik-titik tertentu; properti menguji hukum yang berlaku untuk sebarang input. Contoh: membagi lalu mengalikan kembali harus mengembalikan nilai awal, dan membagi dengan satu tidak mengubah apa pun. Data acaknya dibuat dari generator ber-seed agar hasil selalu sama:

python
import numpy as np

def rasio(a, b):
    a = np.asarray(a, dtype=np.float64)
    b = np.asarray(b, dtype=np.float64)
    with np.errstate(divide="ignore", invalid="ignore", over="ignore"):
        return a / b

def test_properti_bolak_balik():
    rng = np.random.default_rng(42)
    a = rng.uniform(-50, 50, size=6)
    b = rng.uniform(1.0, 9.0, size=6)
    np.testing.assert_allclose(rasio(rasio(a, b), 1 / b), a, atol=1e-9)

def test_properti_identitas():
    rng = np.random.default_rng(42)
    a = rng.uniform(-50, 50, size=6)
    np.testing.assert_array_equal(rasio(a, np.ones_like(a)), a)

test_properti_bolak_balik()
test_properti_identitas()
print("Semua pengujian properti lulus")

Output yang Dihasilkan:

text
Semua pengujian properti lulus

Bedah baris demi baris:

  1. Baris 9-13: generator ber-seed 42 membuat enam pasang data acak yang deterministik; bila implementasi berubah, kasus yang sama persis diuji ulang — bukan data berbeda setiap hari.
  2. Baris 12 (assert_allclose(..., atol=1e-9)): hasil (a/b)/(1/b) secara matematis sama dengan a, tetapi dua kali pembagian float menimbun galat pembulatan; properti yang benar bukan “sama persis” melainkan “berdekatan dalam 1e-9”.
  3. Baris 16-18: properti identitas memakai asersi ketat assert_array_equal karena membagi dengan satu seharusnya tidak mengubah satu bit pun — kontras yang menunjukkan pemilihan asersi mengikuti sifat matematisnya.

Rangkaian yang utuh untuk fungsi numerik kini terbentuk: jalur bahagia, kasus batas (array kosong, nan, pembagian nol, overflow), kontrak exception, dan properti matematis. Bahan yang tersisa hanyalah menjamin seluruh rangkaian itu menghasilkan data yang sama di setiap eksekusi — topik bab penutup section ini.