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
pytest bisa menjalankan test yang ditulis dengan unittest tanpa perubahan apapun. Jadi kamu bisa mulai dengan unittest dan beralih ke pytest kapan 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 deskriptiftest_harga_negatif_harus_error lebih baik dari test_error; test adalah dokumentasi hidup kode kamu.
  • Gunakan assertion yang spesifikassertRaises, assertIn, assertAlmostEqual, assertIsNone menghasilkan pesan error yang jauh lebih informatif dari assertTrue saja.
  • assertRaises untuk menguji eksepsi — pastikan kode melempar eksepsi yang tepat pada kondisi yang salah, bukan hanya kondisi yang benar.
  • setUp/tearDown untuk isolasi — setiap test harus mulai dari kondisi bersih; perubahan di satu test tidak boleh mempengaruhi test lain.
  • setUpClass/tearDownClass untuk resource mahal — koneksi database atau server yang tidak perlu dibuka ulang setiap test.
  • subTest untuk 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.

← Sebelumnya: Web Server   Berikutnya: Mocking →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact