Programming 30 Sep 2026 48 views 0 komentar

Pytest untuk Pemula - Testing Python dari Unit Test sampai Fixture dan Mocking

Pytest untuk Pemula - Testing Python dari Unit Test sampai Fixture dan Mocking

Pernah ngalamin production down gara-gara satu function yang "sebenarnya sederhana" malah bikin error? Saya pernah. Aplikasi berjalan baik di lokal, tapi pas deploy, logic validasi yang kelihatannya trivial malah ngelempar exception yang nggak terduga. Saat itu saya cuma nge-test manual lewat browser, "kayaknya udah ocoy" pikirku. Ternyata edge case yang saya skip saat development jadi masalah di production. Dari situ saya mulai serius nulis test, dan akhirnya milih pytest.

Kenapa pytest dan bukan unittest bawaan Python? Karena pytest bikin nulis test itu menyenangkan. Nggak perlu inheritance, nggak perlu camelCase method, dan assertion-nya cuma pakai assert polos. Tapi yang bikin saya betah adalah fixture dan mocking-nya. Dua fitur ini yang akan kita bahas tuntas di artikel ini.

Apa Itu Pytest dan Kenapa Bukan unittest?

Python sudah punya unittest di standard library. Bisa dipakai tanpa install apapun. Tapi kalau kamu pernah nulis test pakai unittest, tahu rasanya: class inheritance, method setUp dan tearDown, assertion kayak self.assertEqual(a, b) yang panjang banget. Pytest bikin semuanya lebih Pythonic.

Instalasi-nya simpel:

pip install pytest

Bandingkan cara nulis test antara unittest dan pytest:

# unittest style -- verbos
import unittest

class TestCalculator(unittest.TestCase):
 def setUp(self):
 self.calc = Calculator()
 
 def test_add(self):
 self.assertEqual(self.calc.add(2, 3), 5)

# pytest style -- bersih dan langsung
def test_add():
 calc = Calculator()
 assert calc.add(2, 3) == 5

Begitu lihat perbandingannya, pilihan jadi jelas. Pytest lebih ringkas, lebih readable, dan nggak butuh boilerplate. Assertion pakai assert native Python, jadi kalau kamu sudah bisa Python, kamu sudah bisa nulis test pakai pytest.

Struktur Project yang Rapi

Sebelum nulis test, atur struktur folder dulu. Ini penting biar pytest bisa discover test file otomatis dan import-nya nggak berantakan:

myproject/
|-- src/
| `-- calculator.py
|-- tests/
| |-- conftest.py
| |-- test_calculator.py
| `-- test_user.py
|-- pyproject.toml
`-- requirements.txt

File conftest.py adalah tempat kamu naruh shared fixture. Pytest otomatis load file ini tanpa perlu import manual. Ini salah satu magic yang bikin pytest produktif banget untuk project skala menengah ke atas.

Unit Test Dasar

Mari kita mulai dengan contoh nyata. Bikin file calculator.py:

# src/calculator.py

class Calculator:
 def add(self, a, b):
 return a + b
 
 def divide(self, a, b):
 if b == 0:
 raise ValueError("Nggak bisa bagi nol, bro")
 return a / b
 
 def factorial(self, n):
 if n < 0:
 raise ValueError("Faktorial angka negatif? Nggak ada")
 if n == 0:
 return 1
 return n * self.factorial(n - 1)

Sekarang test-nya:

# tests/test_calculator.py
from src.calculator import Calculator

def test_add():
 calc = Calculator()
 assert calc.add(2, 3) == 5
 assert calc.add(-1, 1) == 0
 assert calc.add(0, 0) == 0

def test_add_float():
 calc = Calculator()
 assert calc.add(0.1, 0.2) == 0.3 # ini akan fail!

def test_divide():
 calc = Calculator()
 assert calc.divide(10, 2) == 5

def test_divide_by_zero():
 calc = Calculator()
 import pytest
 with pytest.raises(ValueError, match="bagi nol"):
 calc.divide(10, 0)

def test_factorial():
 calc = Calculator()
 assert calc.factorial(0) == 1
 assert calc.factorial(5) == 120

Ada satu test yang sengaja saya tulis untuk fail: test_add_float. Floating point di Python (dan hampir semua bahasa) punya masalah presisi. 0.1 + 0.2 di Python hasilnya 0.30000000000000004, bukan 0.3. Ini bukan bug, ini cara floating point bekerja di komputer. Test ini mengajar kamu untuk selalu hati-hati dengan float.

Untuk fix test itu, pakai pytest.approx:

def test_add_float():
 calc = Calculator()
 assert calc.add(0.1, 0.2) == pytest.approx(0.3)

Jalankan semua test:

pytest tests/ -v

Flag -v (verbose) menampilkan nama setiap test yang dijalankan. Pas development, saya selalu pakai flag ini biar tahu test mana yang fail.

Fixture - Setup yang Bisa Dipakai Ulang

Bayangkan kamu punya 10 test function yang semua butuh instance Calculator. Tanpa fixture, kamu instantiate di setiap function. Repot dan boros. Fixture solve masalah ini.

# tests/conftest.py
import pytest
from src.calculator import Calculator

@pytest.fixture
def calc():
 """Bikin instance Calculator baru untuk setiap test."""
 return Calculator()

@pytest.fixture
def calc_with_history():
 """Calculator yang sudah punya beberapa operasi."""
 c = Calculator()
 c.add(1, 2)
 c.add(3, 4)
 return c

Sekarang di test file, cukup pass nama fixture sebagai parameter:

# tests/test_calculator.py
def test_add(calc):
 assert calc.add(2, 3) == 5

def test_divide(calc):
 assert calc.divide(10, 2) == 5

def test_factorial(calc):
 assert calc.factorial(5) == 120

Default-nya, fixture jalan sekali per test function. Kalau kamu punya 10 test yang pakai fixture calc, pytest bikin 10 instance berbeda. Ini behavior yang aman karena setiap test dapat state yang bersih.

Tapi kadang kamu mau setup yang mahal (koneksi database, misalnya) cuma dijalankan sekali untuk semua test. Pakai scope:

@pytest.fixture(scope="session")
def db_connection():
 """Koneksi DB sekali untuk seluruh test session."""
 conn = create_connection("postgresql://localhost/testdb")
 yield conn # pakai yield, bukan return
 conn.close() # cleanup setelah semua test selesai

yield di sini penting banget. Semua yang ada setelah yield jadi teardown logic. Jadi kalau kamu buka koneksi DB sebelum yield, dan tutup setelahnya, pytest otomatis handle cleanup bahkan kalau test fail. Scope yang tersedia: function (default), class, module, package, session.

Parametrize - Test dengan Banyak Input

Salah satu fitur pytest yang paling hemat waktu adalah @pytest.mark.parametrize. Daripada nulis 5 test function untuk 5 input berbeda, kamu tulis satu function dengan data yang di-loop:

@pytest.mark.parametrize("a, b, expected", [
 (1, 2, 3),
 (0, 0, 0),
 (-1, 1, 0),
 (100, 200, 300),
 (-5, -3, -8),
])
def test_add_multiple(calc, a, b, expected):
 assert calc.add(a, b) == expected

Pytest otomatis generate 5 test case dari satu function. Kalau salah satu input fail, pytest kasih tahu input mana yang bermasalah. Ini sangat berguna untuk edge case testing kamu tinggal tambah data ke list tanpa nulis function baru.

Mocking - Test Tanpa Dependency Eksternal

Mocking adalah teknik untuk mengganti dependency eksternal (API call, database, file system) dengan object palsu yang behavior-nya bisa kamu kontrol. Kenapa penting? Karena test yang butuh internet atau DB akan lambat, flaky, dan susah di-debug kalau gagal.

Bayangkan kamu punya function yang panggil API eksternal:

# src/weather.py
import requests

class WeatherService:
 def __init__(self, api_key):
 self.api_key = api_key
 self.base_url = "https://api.weatherapi.com/v1"
 
 def get_temperature(self, city):
 url = f"{self.base_url}/current.json"
 params = {"key": self.api_key, "q": city}
 response = requests.get(url, params=params)
 response.raise_for_status()
 data = response.json()
 return data["current"]["temp_c"]

Tanpa mocking, setiap kali test jalan, kamu hit API asli. Kalau API down, test fail. Kalau rate limit kepenuhan, test fail. Kalau internet putus, test fail. Semua failure ini bukan karena kode kamu salah, tapi karena dependency eksternal. Mocking solve ini:

# tests/test_weather.py
from unittest.mock import patch, Mock
from src.weather import WeatherService

@patch("src.weather.requests.get")
def test_get_temperature(mock_get):
 # Arrange - setup mock response
 mock_response = Mock()
 mock_response.json.return_value = {
 "current": {"temp_c": 28.5}
 }
 mock_response.raise_for_status = Mock()
 mock_get.return_value = mock_response
 
 # Act
 service = WeatherService("fake-api-key")
 temp = service.get_temperature("Jakarta")
 
 # Assert
 assert temp == 28.5
 mock_get.assert_called_once()

@patch("src.weather.requests.get")
def test_get_temperature_api_error(mock_get):
 mock_response = Mock()
 mock_response.raise_for_status.side_effect = Exception("API Error 500")
 mock_get.return_value = mock_response
 
 service = WeatherService("fake-key")
 
 import pytest
 with pytest.raises(Exception, match="API Error 500"):
 service.get_temperature("Bandung")

Perhatikan @patch("src.weather.requests.get"). Path-nya penting: kamu patch requests.get di modul src.weather, bukan di requests global. Ini adalah mistake yang sering bikin orang bingung. Kalau kamu patch requests.get langsung, pytest akan replace get di module requests asli, yang mungkin nggak efek ke kode kamu kalau import-nya pakai from requests import get.

Menggunakan pytest-mock untuk Syntax Lebih Bersih

Plugin pytest-mock menyediakan fixture mocker yang lebih Pythonic daripada unittest.mock.patch:

pip install pytest-mock
# tests/test_weather.py (dengan pytest-mock)
from src.weather import WeatherService

def test_get_temperature(mocker):
 mock_response = mocker.Mock()
 mock_response.json.return_value = {"current": {"temp_c": 28.5}}
 mock_response.raise_for_status = mocker.Mock()
 
 mocker.patch("src.weather.requests.get", return_value=mock_response)
 
 service = WeatherService("fake-key")
 temp = service.get_temperature("Jakarta")
 
 assert temp == 28.5

Lebih clean. mocker otomatis cleanup mock setelah test selesai, jadi nggak ada side effect ke test lain. Ini salah satu keunggulan pytest-mock dibanding manual patch dengan decorator.

Coverage - Tahu Seberapa Banyak Kode Sudah Dites

Test coverage jawab pertanyaan: "berapa persen kode kamu sudah ada test-nya?" Ini bukan angka sakral yang harus 100%, tapi indikator bagus kalau ada code path yang belum tercover.

pip install pytest-cov
pytest tests/ --cov=src --cov-report=term-missing

Output-nya bakal nunjukkin file, total statement, dan baris mana yang belum dites. Fokus ke function penting dulu, bukan kejar 100% coverage. 80% coverage dengan test yang bermakna lebih baik daripada 100% dengan test yang cuma ngecek assert True.

Test Configuration dengan pyproject.toml

Biar pytest tahu dimana cari test dan source code, tambah konfigurasi di pyproject.toml:

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
addopts = "-v --tb=short"
markers = [
 "slow: test yang lambat, skip dengan -m 'not slow'",
 "integration: test yang butuh database",
]

Sekarang pytest aja tanpa argumen sudah cukup. Flag --tb=short bikin traceback lebih ringkas, dan marker slow/integration bikin kamu bisa filter test:

# Run semua test kecuali yang slow
pytest -m "not slow"

# Cuma run integration test
pytest -m "integration"

Tips Praktis dari Pengalaman

Setelah berbulan-bulan pakai pytest, ini hal-hal yang saya pelajari susah-susah:

  • Nama test harus deskriptif. test_calc nggak ngerti apa yang dites. test_divide_by_zero_raises_value_error jelas. Kalau test fail, kamu tahu persis apa yang rusak tanpa baca kode.
  • Satu test, satu assertion (idealnya). Kalau test punya 5 assert dan yang ketiga fail, kamu nggak tahu hasil assert 4 dan 5. Pisah jadi test terpisah.
  • Test behavior, bukan implementation. Jangan test "apakah function panggil internal helper X". Test "apakah output-nya benar untuk input Y". Internal bisa refactor, test nggak boleh rusak.
  • Pakai tmp_path fixture untuk file I/O. Pytest sediakan direktori temporary otomatis yang dibersihkan setelah test. Nggak perlu manual os.remove.
  • Run test sebelum commit. Setup git pre-commit hook atau pakai pytest --ff (failed first) biar test yang fail dijalankan duluan.

Kesimpulan

Pytest bikin testing di Python jauh lebih menyenangkan dibanding unittest. Assertion yang pakai assert native, fixture yang fleksibel, parametrize yang hemat kode, dan mocking yang clean semua ini bikin kamu lebih produktif nulis test. Dan test yang bagus bukan beban, tapi investasi. Setiap test yang kamu tulis hari ini adalah bug yang nggak akan production di hari esok.

Kalau kamu baru mulai, fokus ke unit test dulu. Belum perlu mock atau fixture yang complex. Tulis 5 test sederhana untuk function kamu yang paling penting. Setelah terbiasa, baru eksplor fixture dan mocking. Apa kamu sudah pakai pytest di project kamu, atau masih pakai unittest? Coba kasih tahu apa yang bikin kamu switch (atau nggak switch) di kolom komentar.


Bagikan artikel ini:

Komentar (0)

Belum ada komentar. Jadilah yang pertama memberikan tanggapan!

Tinggalkan Komentar