Kode database punya tantangan khusus dalam pengujian: ia menyimpan keadaan antar panggilan, menulis ke berkas sungguhan, dan dua test yang berbagi data akan saling mengganggu. Dua alat menyelesaikan semuanya sekaligus — database :memory: yang segar untuk setiap test, dan fixture pytest yang menyiapkannya otomatis. Hasilnya: test database yang berjalan dalam hitungan milidetik, terisolasi satu sama lain, dan bisa diulang tanpa membersihkan apa pun. Pengetahuan dasar pytest sendiri dibahas pada Course Python section Testing dan Code Quality; jika pytest belum terpasang, cukup pip install pytest.
Fixture :memory: per Test #
Prinsip pertama pengujian database: satu test, satu keadaan bersih. Kita capai dengan membuat koneksi :memory: baru — lengkap dengan skema tabelnya — untuk setiap test, lewat fixture bernama con:
import sqlite3
import pytest
@pytest.fixture
def con():
koneksi = sqlite3.connect(":memory:")
koneksi.row_factory = sqlite3.Row
koneksi.execute("PRAGMA foreign_keys = ON")
koneksi.execute(
"CREATE TABLE pelanggan (id INTEGER PRIMARY KEY, nama TEXT NOT NULL UNIQUE)"
)
yield koneksi
koneksi.close()Mari kita bedah fixture tersebut:
@pytest.fixture: menandai fungsicon()sebagai fixture; setiap test yang meminta parameter bernamaconmenerima hasilnya.koneksi = sqlite3.connect(":memory:"): database baru di RAM per test — tidak ada berkas, tidak ada data warisan, tidak bisa bocor ke test lain.koneksi.row_factory = sqlite3.Row: baris hasil test bisa diperiksa berdasarkan nama kolom, sama seperti pola produksi kita.yield koneksi: kode sebelumyieldadalah persiapan; kode setelahnya adalah pembersihan.koneksi.close()di sini dijamin pytest berjalan walau test gagal.
Fungsi test cukup mendeklarasikan parameter con, dan pytest mengurus sisanya. Test yang lupa meminta con tidak akan pernah bisa membaca database test — dependensi yang diperlukan selalu tampak jelas di tanda tangan fungsinya.
Menyusun Data dan Menguji Pembacaan #
Test yang bermakna butuh data. Helper kecil menghindari pengulangan INSERT di mana-mana, sehingga bagian arrange tiap test tetap satu baris:
def isi_pelanggan(koneksi, *nama):
koneksi.executemany(
"INSERT INTO pelanggan (nama) VALUES (?)", [(n,) for n in nama]
)Pola uji dasarnya: tulis lewat jalur yang mau diuji, lalu baca kembali dan bandingkan dengan nilai yang diharapkan:
def test_insert_dan_baca(con):
isi_pelanggan(con, "Budi", "Siti")
hasil = con.execute("SELECT nama FROM pelanggan ORDER BY nama").fetchall()
assert [r["nama"] for r in hasil] == ["Budi", "Siti"]
def test_cari_dengan_id(con):
isi_pelanggan(con, "Budi")
baris = con.execute(
"SELECT nama FROM pelanggan WHERE id = ?", (1,)
).fetchone()
assert baris["nama"] == "Budi"
def test_tabel_kosong_di_test_baru(con):
assert con.execute("SELECT COUNT(*) FROM pelanggan").fetchone()[0] == 0Test ketiga memang prinsip: test_insert_dan_baca menambahkan dua pelanggan, tetapi test_tabel_kosong_di_test_baru tetap melihat tabel kosong — karena setiap test menerima database :memory: yang baru. Tidak ada urutan test yang aman atau tidak aman; semuanya independen.
Menguji Constraint dengan pytest.raises #
Constraint adalah bagian dari kontrak skema, jadi ia pantas punya test sendiri. Pola pytest.raises memastikan statement yang menyalahi aturan justru menolak, bukan diam-diam menyimpan:
def test_nama_harus_unik(con):
isi_pelanggan(con, "Budi")
with pytest.raises(sqlite3.IntegrityError):
con.execute("INSERT INTO pelanggan (nama) VALUES (?)", ("Budi",))Blok with pytest.raises(sqlite3.IntegrityError): lulus justru ketika INSERT kedua melempar IntegrityError — pelanggaran UNIQUE dari Chapter Penanganan Error sqlite3. Jika exception itu tidak muncul, test gagal; jika muncul exception lain, test juga gagal. Kita bahkan bisa memeriksa pesannya lebih jauh dengan pytest.raises(sqlite3.IntegrityError, match="UNIQUE") bila ingin memastikan penyebab spesifiknya.
Berkas Sementara dengan tmp_path #
:memory: tidak bisa menguji satu hal: data yang bertahan melewati penutupan koneksi. Untuk itu, pytest menyediakan fixture tmp_path — direktori sementara unik per test yang otomatis dibersihkan:
def test_persistence_ke_berkas(tmp_path):
berkas = tmp_path / "toko.db"
con1 = sqlite3.connect(berkas)
con1.execute("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, isi TEXT)")
con1.execute("INSERT INTO pesanan (isi) VALUES (?)", ("kopi gayo",))
con1.commit()
con1.close()
con2 = sqlite3.connect(berkas)
assert con2.execute("SELECT COUNT(*) FROM pesanan").fetchone()[0] == 1
con2.close()Koneksi pertama membuat berkas, menulis satu baris, lalu benar-benar menutup koneksi. Koneksi kedua membuka berkas yang sama dan menemukan barisnya — itulah yang tidak bisa dibuktikan dengan :memory:. Gunakan pola ini saat menguji perilaku yang bergantung pada berkas fisik: persistence, ukuran, atau journal mode WAL dari Chapter *Journal Modes dan WAL`.
Menjalankan Test #
Simpan seluruh fixture, helper, dan kelima test di atas dalam satu berkas test_db.py, lalu jalankan:
python3 -m pytest test_db.py -qOutput yang Dihasilkan:
..... [100%]
5 passed in 0.15sLima titik berarti lima test lulus; satu karakter F atau E menggantikan titik untuk test yang gagal. Angka waktu di baris terakhir bervariasi antar mesin dan antar eksekusi — yang penting angkanya tetap kecil, karena :memory: membuat seluruh rangkaian test ini selesai dalam hitungan perlima detik.
Dengan ini perjalanan kita bersama SQLite di Python lengkap: koneksi, query aman, transaction, galat, pola akses, sampai test. Fondasi yang sama terus bekerja saat aplikasi tumbuh — dari skrip kecil di laptop sampai layanan produksi yang sibuk.