Unit Test #
Kode yang tidak diuji adalah kode yang kamu tidak berani refactor. Unit test adalah jaring pengaman yang memungkinkan kamu mengubah implementasi dengan percaya diri — selama semua test masih hijau, perilaku program belum berubah. Python menyediakan modul unittest bawaan yang lengkap untuk menulis test otomatis: kamu mendefinisikan kondisi yang harus dipenuhi, menjalankan test runner, dan Python melaporkan mana yang lulus dan mana yang gagal beserta alasannya.
Anatomi Unit Test #
Setiap unit test dibangun dari tiga bagian yang sering disebut pola Arrange-Act-Assert:
def test_sesuatu(self):
# Arrange: siapkan data dan kondisi awal
kalkulator = Kalkulator()
# Act: jalankan kode yang ingin diuji
hasil = kalkulator.tambah(3, 7)
# Assert: verifikasi hasilnya sesuai ekspektasi
self.assertEqual(hasil, 10)
Struktur ini membuat test mudah dibaca dan dipahami — siapapun yang melihat test bisa langsung tahu apa yang diuji, bagaimana caranya, dan apa yang diharapkan.
Test Case Dasar #
Setiap test ditulis sebagai metode dalam kelas yang mewarisi unittest.TestCase. Nama metode harus dimulai dengan test_ agar dikenali oleh test runner.
Contoh menguji kelas logika bisnis yang nyata — bukan sekadar fungsi add:
import unittest
class KeranjangBelanja:
def __init__(self):
self.items = []
def tambah_item(self, nama, harga, jumlah=1):
if harga <= 0:
raise ValueError("Harga harus lebih dari 0")
if jumlah <= 0:
raise ValueError("Jumlah harus lebih dari 0")
self.items.append({"nama": nama, "harga": harga, "jumlah": jumlah})
def total(self):
return sum(item["harga"] * item["jumlah"] for item in self.items)
def jumlah_item(self):
return sum(item["jumlah"] for item in self.items)
def kosongkan(self):
self.items.clear()
class TestKeranjangBelanja(unittest.TestCase):
def test_keranjang_baru_kosong(self):
keranjang = KeranjangBelanja()
self.assertEqual(keranjang.total(), 0)
self.assertEqual(keranjang.jumlah_item(), 0)
def test_tambah_satu_item(self):
keranjang = KeranjangBelanja()
keranjang.tambah_item("Buku", 50000)
self.assertEqual(keranjang.total(), 50000)
self.assertEqual(keranjang.jumlah_item(), 1)
def test_tambah_beberapa_item(self):
keranjang = KeranjangBelanja()
keranjang.tambah_item("Buku", 50000, jumlah=2)
keranjang.tambah_item("Pena", 5000, jumlah=3)
self.assertEqual(keranjang.total(), 115000) # (50000*2) + (5000*3)
self.assertEqual(keranjang.jumlah_item(), 5)
def test_kosongkan_keranjang(self):
keranjang = KeranjangBelanja()
keranjang.tambah_item("Buku", 50000)
keranjang.kosongkan()
self.assertEqual(keranjang.total(), 0)
if __name__ == "__main__":
unittest.main()
Assertions yang Tepat #
unittest menyediakan banyak assertion method. Menggunakan assertion yang spesifik menghasilkan pesan error yang jauh lebih informatif saat test gagal.
import unittest
class TestAssertions(unittest.TestCase):
def test_equality(self):
self.assertEqual(2 + 2, 4) # a == b
self.assertNotEqual(2 + 2, 5) # a != b
def test_boolean(self):
self.assertTrue(5 > 3) # bool(x) is True
self.assertFalse(3 > 5) # bool(x) is False
def test_identity(self):
self.assertIsNone(None) # x is None
self.assertIsNotNone("ada") # x is not None
def test_membership(self):
self.assertIn("a", ["a", "b", "c"]) # a in b
self.assertNotIn("z", ["a", "b", "c"]) # a not in b
def test_type(self):
self.assertIsInstance(42, int) # isinstance(a, b)
self.assertIsInstance("halo", str)
def test_float(self):
# assertEqual gagal untuk float karena presisi
# ANTI-PATTERN:
# self.assertEqual(0.1 + 0.2, 0.3) → bisa gagal!
# BENAR: gunakan assertAlmostEqual
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=10)
def test_collections(self):
self.assertListEqual([1, 2, 3], [1, 2, 3])
self.assertDictEqual({"a": 1}, {"a": 1})
self.assertSetEqual({1, 2, 3}, {3, 2, 1})
Menguji Eksepsi #
Salah satu hal terpenting yang sering terlewat: memastikan kode melempar eksepsi yang tepat pada kondisi yang salah.
import unittest
class TestEksepsiKeranjang(unittest.TestCase):
def test_harga_nol_harus_error(self):
keranjang = KeranjangBelanja()
# assertRaises memverifikasi eksepsi yang dilempar
with self.assertRaises(ValueError):
keranjang.tambah_item("Buku", 0)
def test_harga_negatif_harus_error(self):
keranjang = KeranjangBelanja()
with self.assertRaises(ValueError):
keranjang.tambah_item("Buku", -1000)
def test_jumlah_nol_harus_error(self):
keranjang = KeranjangBelanja()
with self.assertRaises(ValueError):
keranjang.tambah_item("Buku", 50000, jumlah=0)
def test_pesan_error_harga(self):
keranjang = KeranjangBelanja()
# assertRaisesRegex: verifikasi pesan eksepsi sekaligus
with self.assertRaisesRegex(ValueError, "Harga harus lebih dari 0"):
keranjang.tambah_item("Buku", -100)
Fixtures: setUp dan tearDown #
Fixtures adalah kode yang dijalankan sebelum dan sesudah setiap test untuk menyiapkan kondisi awal yang konsisten. Tanpa fixtures, kamu akan mengulang kode persiapan yang sama di setiap test method.
import unittest
class TestKeranjangDenganFixture(unittest.TestCase):
def setUp(self):
"""Dijalankan sebelum SETIAP test method."""
self.keranjang = KeranjangBelanja()
# setiap test mulai dengan keranjang bersih + item awal
self.keranjang.tambah_item("Buku Python", 120000, jumlah=1)
self.keranjang.tambah_item("Pena", 5000, jumlah=3)
def tearDown(self):
"""Dijalankan setelah SETIAP test method — bersihkan resource."""
# untuk objek sederhana tidak perlu tearDown
# tapi penting untuk menutup file, koneksi DB, dll
pass
def test_total_awal(self):
# setUp sudah menambahkan item, langsung assert
self.assertEqual(self.keranjang.total(), 135000) # 120000 + (5000*3)
def test_tambah_item_baru(self):
self.keranjang.tambah_item("Penggaris", 8000)
self.assertEqual(self.keranjang.total(), 143000)
def test_kosongkan(self):
self.keranjang.kosongkan()
self.assertEqual(self.keranjang.total(), 0)
# setUp di-reset untuk test berikutnya — perubahan di sini tidak mempengaruhi test lain
Class-Level Fixtures #
Untuk setup yang mahal (koneksi database, inisialisasi server) yang tidak perlu diulang per test:
import unittest
class TestDenganSetupKelas(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Dijalankan SEKALI sebelum semua test dalam kelas ini."""
print("\n[Setup] Membuka koneksi database...")
cls.db_connection = {"status": "connected"} # simulasi koneksi DB
@classmethod
def tearDownClass(cls):
"""Dijalankan SEKALI setelah semua test dalam kelas ini selesai."""
print("\n[Teardown] Menutup koneksi database...")
cls.db_connection = None
def setUp(self):
"""Dijalankan sebelum setiap test — gunakan koneksi yang sudah ada."""
self.cursor = {"db": self.db_connection}
def test_query_pertama(self):
self.assertIsNotNone(self.cursor["db"])
def test_query_kedua(self):
self.assertEqual(self.cursor["db"]["status"], "connected")
Urutan eksekusi fixtures:
setUpClass() ← sekali di awal
setUp() ← sebelum test_pertama
test_pertama()
tearDown() ← setelah test_pertama
setUp() ← sebelum test_kedua
test_kedua()
tearDown() ← setelah test_kedua
tearDownClass() ← sekali di akhir
subTest — Banyak Input dalam Satu Test #
Saat kamu perlu menguji fungsi dengan banyak kombinasi input, subTest memungkinkan semua kasus dijalankan meski satu gagal — kamu mendapat laporan lengkap sekaligus, bukan berhenti di kegagalan pertama.
import unittest
def diskon(total, kode):
"""Hitung total setelah diskon berdasarkan kode."""
diskon_map = {
"HEMAT10": 0.10,
"HEMAT20": 0.20,
"HEMAT50": 0.50,
}
persen = diskon_map.get(kode, 0)
return total * (1 - persen)
class TestDiskon(unittest.TestCase):
def test_semua_kode_diskon(self):
kasus = [
# (total, kode, hasil_expected)
(100000, "HEMAT10", 90000),
(100000, "HEMAT20", 80000),
(100000, "HEMAT50", 50000),
(100000, "INVALID", 100000), # kode tidak dikenal → tanpa diskon
(0, "HEMAT10", 0), # total nol → tetap nol
]
for total, kode, expected in kasus:
with self.subTest(total=total, kode=kode):
self.assertEqual(diskon(total, kode), expected)
Organisasi File Test #
Untuk proyek nyata, pisahkan test ke direktori terpisah dengan struktur yang mencerminkan struktur source code:
proyek/
├── src/
│ ├── __init__.py
│ ├── keranjang.py
│ ├── produk.py
│ └── pembayaran.py
└── tests/
├── __init__.py
├── test_keranjang.py ← test untuk keranjang.py
├── test_produk.py ← test untuk produk.py
└── test_pembayaran.py ← test untuk pembayaran.py
Konvensi penamaan: file test dimulai dengan test_, kelas test dimulai dengan Test, metode test dimulai dengan test_.
Menjalankan Test dari Command Line #
# Jalankan satu file test
python -m unittest test_keranjang.py
# Jalankan satu kelas test spesifik
python -m unittest test_keranjang.TestKeranjangBelanja
# Jalankan satu metode test spesifik
python -m unittest test_keranjang.TestKeranjangBelanja.test_total_awal
# Discover dan jalankan semua test dalam direktori tests/
python -m unittest discover -s tests/ -p "test_*.py"
# Verbose mode — tampilkan nama setiap test yang dijalankan
python -m unittest discover -v
# Output contoh verbose:
# test_keranjang_baru_kosong (test_keranjang.TestKeranjangBelanja) ... ok
# test_tambah_satu_item (test_keranjang.TestKeranjangBelanja) ... ok
# test_total_awal (test_keranjang.TestKeranjangDenganFixture) ... ok
# ----------------------------------------------------------------------
# Ran 3 tests in 0.001s
# OK
unittest vs pytest #
unittest adalah modul bawaan yang langsung tersedia tanpa instalasi. Namun banyak tim memilih pytest karena sintaksnya lebih ringkas.
# unittest — perlu kelas dan self.assertEqual
class TestTambah(unittest.TestCase):
def test_tambah_positif(self):
self.assertEqual(tambah(2, 3), 5)
# pytest — cukup fungsi biasa + assert Python standar
def test_tambah_positif():
assert tambah(2, 3) == 5
unittest pytest
──────── ──────
Instalasi Bawaan Python pip install pytest
Sintaks Verbose (kelas) Ringkas (fungsi)
Assertions self.assertEqual assert biasa Python
Fixtures setUp/tearDown @pytest.fixture (lebih fleksibel)
Parametrize subTest @pytest.mark.parametrize
Output Terbatas Lebih informatif saat gagal
Kompatibilitas — Bisa menjalankan test unittest juga
pytestbisa menjalankan test yang ditulis denganunittesttanpa perubahan apapun. Jadi kamu bisa mulai denganunittestdan beralih kepytestkapan saja — atau gunakan keduanya dalam proyek yang sama.
Ringkasan #
- Pola Arrange-Act-Assert — setiap test memiliki tiga bagian: siapkan data, jalankan kode, verifikasi hasil.
- Nama test harus deskriptif —
test_harga_negatif_harus_errorlebih baik daritest_error; test adalah dokumentasi hidup kode kamu.- Gunakan assertion yang spesifik —
assertRaises,assertIn,assertAlmostEqual,assertIsNonemenghasilkan pesan error yang jauh lebih informatif dariassertTruesaja.assertRaisesuntuk menguji eksepsi — pastikan kode melempar eksepsi yang tepat pada kondisi yang salah, bukan hanya kondisi yang benar.setUp/tearDownuntuk isolasi — setiap test harus mulai dari kondisi bersih; perubahan di satu test tidak boleh mempengaruhi test lain.setUpClass/tearDownClassuntuk resource mahal — koneksi database atau server yang tidak perlu dibuka ulang setiap test.subTestuntuk banyak kasus input — semua kasus dijalankan meski satu gagal; kamu mendapat laporan lengkap sekaligus.- Pisahkan test ke direktori
tests/— struktur file test mencerminkan struktur source code untuk kemudahan navigasi.