Belajar Python Unit Test Unittest

| | Python
Belajar Python Unit Test Unittest

nulis unit test itu rasanya nambah kerjaan, sampai project membesar dan satu perubahan kecil bikin fitur lain rusak tanpa ketahuan. sejak saya biasakan nulis test, bug kayak gitu ketahuan dalam detik, bukan setelah user komplain. di sini kalian mulai dari yang paling gampang, pakai unittest bawaan Python, tidak perlu install apa-apa. kalau belum paham try except buat kasus error yang mau di-test, baca dulu error handling di Python.

1. contoh paling gampang

saya punya dua function di file terpisah, ini yang mau saya test:

# main.py
def tambah(a, b):
    return a + b

def bagi(a, b):
    if b == 0:
        raise ValueError("Tidak bisa bagi nol!")
    return a / b

sedikit cerita, tambah sengaja saya bikin tanpa validasi biar simpel, bagi saya kasih raise ValueError kalau pembagi nol, karena itu yang sering kelupaan di test.

sekarang file test-nya:

# test_main.py
import unittest
from main import tambah, bagi

class TestMath(unittest.TestCase):
    def test_tambah(self):
        self.assertEqual(tambah(2, 3), 5)
        self.assertEqual(tambah(-1, 1), 0)
    
    def test_bagi(self):
        self.assertEqual(bagi(10, 2), 5)
        with self.assertRaises(ValueError):
            bagi(10, 0)

if __name__ == "__main__":
    unittest.main()

beberapa catatan yang dulu bikin saya bingung:

  • class TestMath(unittest.TestCase), harus warisi TestCase, kalau tidak self.assertEqual tidak ada
  • nama method harus test_*, tanpa itu runner tidak jalan
  • self.assertEqual(tambah(2, 3), 5), urutannya expected vs actual tidak masalah, tapi biasakan actual dulu biar pesan error enak dibaca
  • with self.assertRaises(ValueError), pakai context manager, jangan assertEqual buat exception

jalankan pakai python -m unittest test_main.py atau cukup python -m unittest biar dia cari semua file test_*.py.

2. assertion yang sering saya pakai

self.assertEqual(a, b)      # a == b
self.assertNotEqual(a, b)   # a != b
self.assertTrue(x)          # bool(x) is True
self.assertFalse(x)         # bool(x) is False
self.assertIn(item, coll)   # item in list/string/dict keys
self.assertNotIn(item, coll)
self.assertIsNone(x)        # x is None
self.assertIsNotNone(x)
self.assertIs(a, b)         # a is b (identity, bukan equality)
self.assertIsInstance(a, int)  # isinstance(a, int)
self.assertRaises(Error)    # expect exception (with context manager)
self.assertRaisesRegex(Error, "pattern")  # expect exception + regex match
self.assertAlmostEqual(a, b, places=7)  # hampir sama (floating point)
self.assertCountEqual(a, b)  # sama isi, beda urutan OK
self.assertGreater(a, b)     # a > b
self.assertLess(a, b)       # a < b
self.assertDictEqual(d1, d2)  # dict sama
self.assertListEqual(l1, l2)  # list sama
self.assertMultiLineEqual(a, b)  # string sama (termasuk newline)

kalau bingung harus pakai yang mana, pakai assertEqual dan assertTrue dulu sudah cukup untuk 80% kasus.

3. setUp dan tearDown, biar tidak copy paste

setUp jalan sebelum tiap test, tearDown sesudah tiap test. enaknya, tiap test dapat database bersih:

class TestDatabase(unittest.TestCase):
    def setUp(self):
        """Jalan SEBELUM setiap test method"""
        self.db = Database(":memory:")
        self.db.connect()
        self.db.create_table()
    
    def tearDown(self):
        """Jalan SETELAH setiap test method"""
        self.db.close()
    
    def test_insert(self):
        # db sudah siap dari setUp
        self.db.insert("users", {"name": "Andi"})
        result = self.db.get("users")
        self.assertEqual(result["name"], "Andi")
    
    def test_update(self):
        # db baru dari setUp (bersih lagi)
        self.db.insert("users", {"name": "Budi"})
        self.db.update("users", {"name": "Budi Santoso"})
        result = self.db.get("users")
        self.assertEqual(result["name"], "Budi Santoso")

yang perlu diingat:

  • setUp jalan dua kali kalau ada dua test, jadi tiap test tidak saling ngotorin
  • kalau setUp yang error, test tidak jalan dan tearDown tidak kepanggil
  • kalau test sukses, tearDown tetap jalan buat bersih-bersih, misal tutup koneksi atau hapus file temporary

4. mocking biar tidak hit internet

mocking itu bikin tiruan supaya test tetap terisolasi:

from unittest.mock import Mock, patch, MagicMock
import requests

def get_user_name(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()["name"]

tanpa mock, function ini akan hit internet, lambat dan flaky. dengan mock:

@patch("requests.get")
def test_get_user_name(mock_get):
    # 1. bikin object mock untuk response
    mock_response = Mock()
    mock_response.json.return_value = {"name": "Andi"}
    
    # 2. set nilai kembalian requests.get
    mock_get.return_value = mock_response
    
    # 3. jalankan function
    result = get_user_name(1)
    
    # 4. assertion
    self.assertEqual(result, "Andi")
    mock_get.assert_called_once_with("https://api.example.com/users/1")

yang sering bikin saya salah di awal, path di @patch harus tempat object dipakai, bukan tempat didefinisikan. jadi kalau get_user_name ada di main.py dan import requests di sana, pakai @patch("main.requests.get") lebih aman.

5. TDD, tulis test dulu baru kode

TDD itu siklusnya RED, GREEN, REFACTOR:

# 1. RED: tulis test dulu (test fails)
# test_calculator.py

import unittest
from calculator import tambah, kali

class TestCalculator(unittest.TestCase):
    def test_tambah(self):
        self.assertEqual(tambah(2, 3), 5)
    
    def test_kali(self):
        self.assertEqual(kali(3, 4), 12)
        self.assertEqual(kali(0, 99), 0)
        self.assertEqual(kali(-2, 3), -6)

# Jalankan: python -m unittest test_calculator.py
# Result: ERROR (calculator module belum ada)

# 2. GREEN: tulis implementasi biar test pass
# calculator.py

def tambah(a, b):
    return a + b

def kali(a, b):
    return a * b

# Jalankan lagi: python -m unittest test_calculator.py
# Result: OK (dua-dua pass)

# 3. REFACTOR: improve kode tanpa break test
# masih aman refactor karena test menjaganya

menurut saya, TDD paling enak buat function kecil yang logic-nya jelas, kalau untuk UI atau eksplorasi, tulis kode dulu baru test juga tidak apa-apa.

6. lihat coverage, jangan kejar 100 persen

# install coverage
pip install coverage

# jalan test dengan coverage
coverage run -m unittest

# lihat laporan
coverage report

contoh output:

Name              Stmts   Miss  Cover
-------------------------------------
calculator.py         8      1    88%
test_calculator.py   11      0   100%
-------------------------------------
TOTAL               19      1    89%

Cover 88% artinya satu baris di calculator.py belum ke-test. lihat baris mana:

coverage report -m
# Tambah kolom "Missing" yang menunjukkan baris yang belum tercakup

html yang lebih enak dilihat:

coverage html
# Buka htmlcov/index.html di browser

7. struktur folder biar rapi

my_project/
├── src/
│   ├── calculator.py
│   └── api.py
├── tests/
│   ├── __init__.py
│   ├── test_calculator.py
│   ├── test_api.py
│   └── fixtures/
│       └── sample_data.json
├── .coveragerc
└── pytest.ini

isi .coveragerc yang saya pakai:

[run]
source = src
omit = tests/*

[report]
exclude_lines =
    pragma: no cover
    if __name__ == .__main__.:
    raise NotImplementedError

8. beberapa kebiasaan yang saya pegang

  • satu test fokus satu hal, kalau gagal di tengah jadi gampang debug
  • nama test yang jelas, test_user_login_success lebih enak dari test1
  • tiap test berdiri sendiri, jangan tergantung urutan eksekusi
  • pakai pola AAA, Arrange siapin data, Act jalankan aksi, Assert cek hasil
  • jangan test implementasi, test behavior, cek output bukan cara loop di dalam
  • coverage 70-80 persen sudah bagus, 100 persen bukan tujuan, yang penting assertion-nya berkualitas
  • test happy path plus edge case, jangan cuma kasus normal, coba kosong, nol, negatif, dan sangat besar
  • commit test bareng kode, kalau push kode tanpa test, kualitasnya pelan-pelan turun

9. kalau mau coba pytest

pytest lebih ringkas, install dulu pip install pytest:

# test_calc.py (pytest)
import pytest
from calculator import tambah, kali

def test_tambah():
    assert tambah(2, 3) == 5
    assert tambah(-1, 1) == 0

def test_kali():
    assert kali(3, 4) == 12
    assert kali(0, 99) == 0

def test_bagi_zero():
    with pytest.raises(ValueError):
        bagi(10, 0)

beda singkat:

Aspekunittestpytest
InstallBawaan Pythonpip install
Syntaxclass + self.assertEqualassert langsung
CLIpython -m unittestpytest
FixturesetUp/tearDown@pytest.fixture lebih fleksibel
Mockingunittest.mockunittest.mock juga bisa
CommunitystdlibSangat besar

kalau baru belajar, unittest sudah cukup. kalau project mulai besar, pindah ke pytest pelan-pelan.

Kesimpulan

  • unittest itu stdlib, tidak perlu install, file test namanya test_*.py
  • warisin unittest.TestCase, method test_* otomatis jalan
  • assertion paling sering assertEqual, assertTrue, assertRaises, assertIn
  • setUp sebelum tiap test, tearDown sesudahnya, cocok buat inisialisasi dan cleanup
  • @patch buat isolasi, mock biar tidak hit API beneran
  • TDD itu RED tulis test dulu, GREEN bikin pass, REFACTOR rapikan
  • coverage pakai coverage run -m unittest lalu coverage report, target 70-80 persen
  • untuk project kecil, unittest enak, untuk besar, pytest lebih fleksibel

jalankan pakai python -m unittest buat cari semua test_*.py, atau pytest kalau sudah pakai pytest.

Baca Juga Mengenai :


Pertanyaan yang Sering Diajukan

Apa itu unit test?

unit test adalah test otomatis untuk bagian kode terkecil (unit) secara terisolasi. tujuannya memastikan setiap function/class behave sesuai ekspektasi. bedanya dengan integration test: unit test test satu unit saja tanpa dependensi luar, integration test test banyak komponen jalan bareng.

Kenapa harus menulis unit test?

tiga alasan utama: (1) bug ketangkep sebelum production, (2) refactoring jadi aman karena ada test menjagga, (3) dokumentasi hidup yang selalu sinkron dengan kode. studi menunjukkan 15 menit menulis test menghemat 67 menit debugging.

Bedanya unittest dan pytest?

unittest adalah modul bawaan Python (stdlib), tidak perlu install. pytest library eksternal dengan syntax lebih simpel dan powerful. untuk proyek baru, pytest lebih direkomendasikan karena assertion yang lebih readable, fixture yang lebih fleksibel, dan komunitas yang besar. tapi unittest tetap bagus untuk belajar dan project kecil.

Apa itu TDD (Test-Driven Development)?

TDD adalah workflow: tulis test dulu (RED), implementasi biar gagal, lalu tulis kode sampai test pass (GREEN), refactor kode tanpa break test (REFACTOR). cycle ini bikin kode jadi clean dan testable dari awal. manfaat: design lebih baik, confidence tinggi, regression tertangkep dini.

Apa itu mocking?

mocking adalah membuat tiruan (fake) dari dependensi eksternal supaya test tetap terisolasi. contoh: test function yang pakai API, tapi tidak mau benar-benar fetch dari internet. pakai unittest.mock untuk bikin object tiruan yang return data palsu. benefit: test cepat, tidak flaky, tidak butuh internet.

Apa itu test coverage?

test coverage mengukur berapa persen kode yang tertutup test. 80% coverage artinya 80% baris kode pernah dijalankan minimal satu test. coverage bukan tujuan akhir: 100% coverage bukan jaminan bebas bug, tapi bagus untuk identifier kode yang belum pernah di-test. pakai coverage.py: pip install coverage; coverage run -m unittest; coverage report.

Kapan tidak perlu unit test?

untuk utility script satu pakai: tidak perlu test. untuk prototype cepat: test bisa ditambahkan depois. untuk kode yang akan dihapus: tidak masuk akal. untuk kode sederhana yang ekspektasi behavior-nya jelas dan jarang berubah: risk rendah. tapi kalau kode itu critical path atau akan di-maintain lama, test tetap worth it.

Apa itu setUp dan tearDown?

setUp() jalan sebelum setiap test method, biasa untuk inisialisasi object atau database. tearDown() jalan sesudah setiap test, biasa untuk cleanup (tutup koneksi, hapus file temporary). keduanya otomatis dipanggil oleh unittest framework. kalau ada exception di setUp, tearDown tidak jalan.

Sigit Nurhanafi avatar
Full-stack developer & technical writer. Berpengalaman di PHP, Laravel, NodeJS, MySQL, dan Python. Aktif menulis tutorial pemrograman dan maintaining open-source projects di PemburuKode sejak 2021.