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 tidakself.assertEqualtidak 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 dibacawith self.assertRaises(ValueError), pakai context manager, janganassertEqualbuat 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:
setUpjalan dua kali kalau ada dua test, jadi tiap test tidak saling ngotorin- kalau
setUpyang error, test tidak jalan dantearDowntidak kepanggil - kalau test sukses,
tearDowntetap 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_successlebih enak daritest1 - 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:
| Aspek | unittest | pytest |
|---|---|---|
| Install | Bawaan Python | pip install |
| Syntax | class + self.assertEqual | assert langsung |
| CLI | python -m unittest | pytest |
| Fixture | setUp/tearDown | @pytest.fixture lebih fleksibel |
| Mocking | unittest.mock | unittest.mock juga bisa |
| Community | stdlib | Sangat 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, methodtest_*otomatis jalan - assertion paling sering
assertEqual,assertTrue,assertRaises,assertIn setUpsebelum tiap test,tearDownsesudahnya, cocok buat inisialisasi dan cleanup@patchbuat isolasi, mock biar tidak hit API beneran- TDD itu RED tulis test dulu, GREEN bikin pass, REFACTOR rapikan
- coverage pakai
coverage run -m unittestlalucoverage 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.

