Uji Unit dalam Python: Panduan Lengkap dengan Contoh
Pengujian Python dengan pytest, TDD, mocking, dan coverage
Pengujian unit memastikan kode Python Anda berjalan dengan benar dan terus berfungsi seiring berkembangnya proyek Anda. Panduan komprehensif ini mencakup segala sesuatu yang perlu Anda ketahui tentang pengujian unit di Python, mulai dari konsep dasar hingga teknik lanjutan.

Mengapa Pengujian Unit Penting
Pengujian unit memberikan banyak manfaat bagi pengembang Python:
- Deteksi Bug Dini: Menangkap bug sebelum mencapai produksi
- Kualitas Kode: Memaksa Anda menulis kode modular dan dapat diuji
- Keyakinan Saat Refaktorisasi: Lakukan perubahan dengan aman karena pengujian akan menangkap regresi
- Dokumentasi: Pengujian berfungsi sebagai dokumentasi eksekusi dari cara kode seharusnya bekerja
- Pengembangan Lebih Cepat: Pengujian otomatis lebih cepat daripada pengujian manual
- Desain yang Lebih Baik: Menulis kode yang dapat diuji mengarah ke arsitektur yang lebih baik
Memahami Dasar-Dasar Pengujian Unit
Apa Itu Pengujian Unit?
Pengujian unit memverifikasi bagian terkecil dari aplikasi yang dapat diuji (biasanya fungsi atau metode) secara terisolasi. Ini harus:
- Cepat: Berjalan dalam milidetik
- Terisolasi: Tidak bergantung pada pengujian lain atau sistem eksternal
- Dapat Diulang: Menghasilkan hasil yang sama setiap kali
- Mengvalidasi Diri Sendiri: Lulus atau gagal secara jelas tanpa inspeksi manual
- Tepat Waktu: Ditulis sebelum atau bersamaan dengan kode
Piramida Pengujian
Suite pengujian yang sehat mengikuti piramida pengujian:
/\
/ \ Pengujian E2E (Sedikit)
/____\
/ \ Pengujian Integrasi (Beberapa)
/_____ ___\
/ \ Pengujian Unit (Banyak)
/_____ __ ___\
Pengujian unit membentuk fondasi - mereka banyak, cepat, dan memberikan umpan balik cepat.
Perbandingan Kerangka Kerja Pengujian Python
unittest: Kerangka Kerja Bawaan
Perpustakaan standar Python mencakup unittest, yang terinspirasi oleh JUnit:
import unittest
class TestCalculator(unittest.TestCase):
def setUp(self):
"""Dijalankan sebelum setiap pengujian"""
self.calc = Calculator()
def tearDown(self):
"""Dijalankan setelah setiap pengujian"""
self.calc = None
def test_addition(self):
result = self.calc.add(2, 3)
self.assertEqual(result, 5)
def test_division_by_zero(self):
with self.assertRaises(ZeroDivisionError):
self.calc.divide(10, 0)
if __name__ == '__main__':
unittest.main()
Kelebihan:
- Bawaan, tidak perlu instalasi
- Cocok untuk pengembang yang sudah familiar dengan kerangka kerja xUnit
- Ramah perusahaan, sudah mapan
Kekurangan:
- Sintaks yang rumit dengan boilerplate
- Memerlukan kelas untuk organisasi pengujian
- Manajemen fixture yang kurang fleksibel
pytest: Pilihan Modern
pytest adalah kerangka pengujian pihak ketiga yang paling populer:
import pytest
def test_addition():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_division_by_zero():
calc = Calculator()
with pytest.raises(ZeroDivisionError):
calc.divide(10, 0)
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
])
def test_addition_parametrized(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
Kelebihan:
- Sintaks sederhana, Pythonic
- Sistem fixture yang kuat
- Ekosistem plugin yang hebat
- Pelaporan kesalahan yang lebih baik
- Pengujian parametrized bawaan
Kekurangan:
- Memerlukan instalasi
- Kurang familiar bagi pengembang dari bahasa lain
Instalasi:
pip install pytest pytest-cov pytest-mock
Menulis Pengujian Unit Pertama Anda
Mari kita bangun contoh sederhana dari awal menggunakan Pengembangan Berbasis Pengujian (TDD). Jika Anda baru dengan Python atau membutuhkan referensi cepat untuk sintaks dan fitur bahasa, lihat Python Cheatsheet kami untuk tinjauan komprehensif tentang dasar-dasar Python.
Contoh: Fungsi Utilitas String
Langkah 1: Tulis Pengujian Terlebih Dahulu (Merah)
Buat test_string_utils.py:
import pytest
from string_utils import reverse_string, is_palindrome, count_vowels
def test_reverse_string():
assert reverse_string("hello") == "olleh"
assert reverse_string("") == ""
assert reverse_string("a") == "a"
def test_is_palindrome():
assert is_palindrome("racecar") == True
assert is_palindrome("hello") == False
assert is_palindrome("") == True
assert is_palindrome("A man a plan a canal Panama") == True
def test_count_vowels():
assert count_vowels("hello") == 2
assert count_vowels("HELLO") == 2
assert count_vowels("xyz") == 0
assert count_vowels("") == 0
Langkah 2: Tulis Kode Minimal untuk Melalui Pengujian (Hijau)
Buat string_utils.py:
def reverse_string(s: str) -> str:
"""Balikkan string"""
return s[::-1]
def is_palindrome(s: str) -> bool:
"""Periksa apakah string adalah palindrome (mengabaikan huruf besar kecil dan spasi)"""
cleaned = ''.join(s.lower().split())
return cleaned == cleaned[::-1]
def count_vowels(s: str) -> int:
"""Hitung vokal dalam string"""
return sum(1 for char in s.lower() if char in 'aeiou')
Langkah 3: Jalankan Pengujian
pytest test_string_utils.py -v
Keluaran:
test_string_utils.py::test_reverse_string PASSED
test_string_utils.py::test_is_palindrome PASSED
test_string_utils.py::test_count_vowels PASSED
Teknik Pengujian Lanjutan
Menggunakan Fixture untuk Pengaturan Pengujian
Fixture menyediakan pengaturan dan penyelesaian pengujian yang dapat digunakan kembali:
import pytest
from database import Database
@pytest.fixture
def db():
"""Buat database pengujian"""
database = Database(":memory:")
database.create_tables()
yield database # Berikan ke pengujian
database.close() # Bersihkan setelah pengujian
@pytest.fixture
def sample_users(db):
"""Tambahkan pengguna contoh ke database"""
db.add_user("Alice", "alice@example.com")
db.add_user("Bob", "bob@example.com")
return db
def test_get_user(sample_users):
user = sample_users.get_user_by_email("alice@example.com")
assert user.name == "Alice"
def test_user_count(sample_users):
assert sample_users.count_users() == 2
Ruang Lingkup Fixture
Kontrol masa hidup fixture dengan ruang lingkup:
@pytest.fixture(scope="function") # Default: dijalankan untuk setiap pengujian
def func_fixture():
return create_resource()
@pytest.fixture(scope="class") # Sekali per kelas pengujian
def class_fixture():
return create_resource()
@pytest.fixture(scope="module") # Sekali per modul
def module_fixture():
return create_resource()
@pytest.fixture(scope="session") # Sekali per sesi pengujian
def session_fixture():
return create_expensive_resource()
Menyamarakan Ketergantungan Eksternal
Gunakan penyamaran untuk memisahkan kode dari ketergantungan eksternal:
from unittest.mock import Mock, patch, MagicMock
import requests
class WeatherService:
def get_temperature(self, city):
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temp"]
# Pengujian dengan penyamaran
def test_get_temperature():
service = WeatherService()
# Menyamar fungsi requests.get
with patch('requests.get') as mock_get:
# Konfigurasi respons penyamaran
mock_response = Mock()
mock_response.json.return_value = {"temp": 72}
mock_get.return_value = mock_response
# Pengujian
temp = service.get_temperature("Boston")
assert temp == 72
# Verifikasi pemanggilan
mock_get.assert_called_once_with("https://api.weather.com/Boston")
Menggunakan Plugin pytest-mock
pytest-mock menyediakan sintaks yang lebih bersih:
def test_get_temperature(mocker):
service = WeatherService()
# Menyamar menggunakan pytest-mock
mock_response = mocker.Mock()
mock_response.json.return_value = {"temp": 72}
mocker.patch('requests.get', return_value=mock_response)
temp = service.get_temperature("Boston")
assert temp == 72
Pengujian Parametrized
Uji berbagai skenario secara efisien:
import pytest
@pytest.mark.parametrize("input,expected", [
("", True),
("a", True),
("ab", False),
("aba", True),
("racecar", True),
("hello", False),
])
def test_is_palindrome_parametrized(input, expected):
assert is_palindrome(input) == expected
@pytest.mark.parametrize("number,is_even", [
(0, True),
(1, False),
(2, True),
(-1, False),
(-2, True),
])
def test_is_even(number, is_even):
assert (number % 2 == 0) == is_even
Pengujian Eksepsi dan Penanganan Kesalahan
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Tidak dapat membagi dengan nol")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Tidak dapat membagi dengan nol"):
divide(10, 0)
def test_divide_by_zero_with_message():
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "nol" in str(exc_info.value).lower()
# Pengujian bahwa tidak ada eksepsi yang diangkat
def test_divide_success():
result = divide(10, 2)
assert result == 5.0
Pengujian Kode Asinkron
Pengujian kode asinkron sangat penting untuk aplikasi Python modern, terutama ketika bekerja dengan API, database, atau layanan AI. Berikut cara menguji fungsi asinkron:
import pytest
import asyncio
async def fetch_data(url):
"""Fungsi asinkron untuk mengambil data"""
await asyncio.sleep(0.1) # Simulasikan panggilan API
return {"status": "success"}
@pytest.mark.asyncio
async def test_fetch_data():
result = await fetch_data("https://api.example.com")
assert result["status"] == "success"
@pytest.mark.asyncio
async def test_fetch_data_with_mock(mocker):
# Menyamar fungsi asinkron
mock_fetch = mocker.AsyncMock(return_value={"status": "mocked"})
mocker.patch('module.fetch_data', mock_fetch)
result = await fetch_data("https://api.example.com")
assert result["status"] == "mocked"
Untuk contoh praktis pengujian kode asinkron dengan layanan AI, lihat panduan kami tentang mengintegrasikan Ollama dengan Python, yang mencakup strategi pengujian untuk interaksi LLM.
Cakupan Kode
Ukur seberapa banyak kode Anda yang diuji:
Menggunakan pytest-cov
# Jalankan pengujian dengan cakupan
pytest --cov=myproject tests/
# Hasilkan laporan HTML
pytest --cov=myproject --cov-report=html tests/
# Tunjukkan baris yang hilang
pytest --cov=myproject --cov-report=term-missing tests/
Konfigurasi Cakupan
Buat .coveragerc:
[run]
source = myproject
omit =
*/tests/*
*/venv/*
*/__pycache__/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
Praktik Terbaik Cakupan
- Targetkan 80%+ cakupan untuk jalur kode kritis
- Jangan terlalu memperhatikan 100% - fokus pada pengujian yang bermakna
- Uji kasus batas bukan hanya jalur yang sukses
- Keluarkan boilerplate dari laporan cakupan
- Gunakan cakupan sebagai panduan bukan tujuan
Organisasi Pengujian dan Struktur Proyek
Struktur yang Direkomendasikan
myproject/
├── myproject/
│ ├── __init__.py
│ ├── module1.py
│ ├── module2.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Fixture bersama
│ ├── test_module1.py
│ ├── test_module2.py
│ ├── test_utils.py
│ └── integration/
│ ├── __init__.py
│ └── test_integration.py
├── pytest.ini
├── requirements.txt
└── requirements-dev.txt
conftest.py untuk Fixture Bersama
# tests/conftest.py
import pytest
from myproject.database import Database
@pytest.fixture(scope="session")
def test_db():
"""Database pengujian seluruh sesi"""
db = Database(":memory:")
db.create_schema()
yield db
db.close()
@pytest.fixture
def clean_db(test_db):
"""Database bersih untuk setiap pengujian"""
test_db.clear_all_tables()
return test_db
Konfigurasi pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--strict-markers
--cov=myproject
--cov-report=term-missing
--cov-report=html
markers =
slow: menandai pengujian sebagai lambat
integration: menandai pengujian sebagai pengujian integrasi
unit: menandai pengujian sebagai pengujian unit
Praktik Terbaik untuk Pengujian Unit
1. Ikuti Pola AAA
Arrange-Act-Assert membuat pengujian jelas:
def test_user_creation():
# Arrange
username = "john_doe"
email = "john@example.com"
# Act
user = User(username, email)
# Assert
assert user.username == username
assert user.email == email
assert user.is_active == True
2. Satu Pernyataan Per Pengujian (Petunjuk, Bukan Aturan)
# Baik: Pengujian fokus
def test_user_username():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
def test_user_email():
user = User("john_doe", "john@example.com")
assert user.email == "john@example.com"
# Juga diterima: Pernyataan terkait
def test_user_creation():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
assert user.email == "john@example.com"
assert isinstance(user.created_at, datetime)
3. Gunakan Nama Pengujian yang Deskriptif
# Buruk
def test_user():
pass
# Baik
def test_user_creation_with_valid_data():
pass
def test_user_creation_fails_with_invalid_email():
pass
def test_user_password_is_hashed_after_setting():
pass
4. Uji Kasus Batas dan Batas
def test_age_validation():
# Kasus valid
assert validate_age(0) == True
assert validate_age(18) == True
assert validate_age(120) == True
# Kasus batas
assert validate_age(-1) == False
assert validate_age(121) == False
# Kasus batas
with pytest.raises(TypeError):
validate_age("18")
with pytest.raises(TypeError):
validate_age(None)
5. Pertahankan Pengujian yang Mandiri
# Buruk: Pengujian bergantung pada urutan
counter = 0
def test_increment():
global counter
counter += 1
assert counter == 1
def test_increment_again(): # Gagal jika dijalankan sendiri
global counter
counter += 1
assert counter == 2
# Baik: Pengujian mandiri
def test_increment():
counter = Counter()
counter.increment()
assert counter.value == 1
def test_increment_multiple_times():
counter = Counter()
counter.increment()
counter.increment()
assert counter.value == 2
6. Jangan Uji Detail Implementasi
# Buruk: Mengujikan implementasi
def test_sort_uses_quicksort():
sorter = Sorter()
assert sorter.algorithm == "quicksort"
# Baik: Mengujikan perilaku
def test_sort_returns_sorted_list():
sorter = Sorter()
result = sorter.sort([3, 1, 2])
assert result == [1, 2, 3]
7. Uji Kasus Penggunaan Dunia Nyata
Ketika menguji perpustakaan yang memproses atau mengubah data, fokuslah pada skenario dunia nyata. Misalnya, jika Anda bekerja dengan pengambilan data web atau konversi konten, lihat panduan kami tentang mengubah HTML ke Markdown dengan Python, yang mencakup strategi pengujian dan perbandingan benchmark untuk berbagai perpustakaan konversi.
Integrasi dengan Continuous Integration
Contoh GitHub Actions
# .github/workflows/tests.yml
name: Pengujian
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, 3.10, 3.11, 3.12]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run tests
run: |
pytest --cov=myproject --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Pengujian Fungsi Serverless
Ketika menguji fungsi AWS Lambda atau aplikasi serverless, pertimbangkan strategi pengujian integrasi bersama dengan pengujian unit. Panduan kami tentang membangun AWS Lambda ganda dengan Python dan Terraform mencakup pendekatan pengujian untuk aplikasi Python serverless, termasuk cara menguji penangan Lambda, konsumen SQS, dan integrasi API Gateway.
Daftar Pemeriksaan Praktik Terbaik Pengujian
- Tulis pengujian sebelum atau bersamaan dengan kode (TDD)
- Pertahankan pengujian yang cepat (< 1 detik per pengujian)
- Buat pengujian mandiri dan terisolasi
- Gunakan nama pengujian yang deskriptif
- Ikuti pola AAA (Arrange-Act-Assert)
- Uji kasus batas dan kondisi kesalahan
- Sisipkan ketergantungan eksternal
- Targetkan cakupan kode 80%+
- Jalankan pengujian dalam pipeline CI/CD
- Tinjau dan refactor pengujian secara berkala
- Dokumentasikan skenario pengujian kompleks
- Gunakan fixture untuk pengaturan umum
- Parametriskan pengujian yang mirip
- Pertahankan pengujian sederhana dan mudah dibaca
Pola Pengujian Umum
Pengujian Kelas
class TestUser:
@pytest.fixture
def user(self):
return User("john_doe", "john@example.com")
def test_username(self, user):
assert user.username == "john_doe"
def test_email(self, user):
assert user.email == "john@example.com"
def test_full_name(self, user):
user.first_name = "John"
user.last_name = "Doe"
assert user.full_name() == "John Doe"
Pengujian dengan File Sementara
import pytest
from pathlib import Path
@pytest.fixture
def temp_file(tmp_path):
"""Buat file sementara"""
file_path = tmp_path / "test_file.txt"
file_path.write_text("test content")
return file_path
def test_read_file(temp_file):
content = temp_file.read_text()
assert content == "test content"
Pengujian Pembuatan File
Ketika menguji kode yang menghasilkan file (seperti PDF, gambar, atau dokumen), gunakan direktori sementara dan verifikasi properti file:
@pytest.fixture
def temp_output_dir(tmp_path):
"""Sediakan direktori output sementara"""
output_dir = tmp_path / "output"
output_dir.mkdir()
return output_dir
def test_pdf_generation(temp_output_dir):
pdf_path = temp_output_dir / "output.pdf"
generate_pdf(pdf_path, content="Test")
assert pdf_path.exists()
assert pdf_path.stat().st_size > 0
Untuk contoh komprehensif pengujian pembuatan PDF, lihat panduan kami tentang membuat PDF dalam Python, yang mencakup strategi pengujian untuk berbagai perpustakaan PDF.
Pengujian dengan Monkeypatch
def test_environment_variable(monkeypatch):
monkeypatch.setenv("API_KEY", "test_key_123")
assert os.getenv("API_KEY") == "test_key_123"
def test_module_attribute(monkeypatch):
monkeypatch.setattr("module.CONSTANT", 42)
assert module.CONSTANT == 42
Tautan dan Sumber Daya Berguna
- Dokumentasi pytest
- Dokumentasi unittest
- Plugin pytest-cov
- Plugin pytest-mock
- Dokumentasi unittest.mock
- Test Driven Development by Example - Kent Beck
- Python Testing with pytest - Brian Okken
Sumber Daya Terkait
Dasar-Dasar Python dan Praktik Terbaik
- Python Cheatsheet - Referensi komprehensif untuk sintaks Python, struktur data, dan pola umum
Pengujian Kasus Penggunaan Python Spesifik
- Mengintegrasikan Ollama dengan Python - Strategi pengujian untuk integrasi LLM dan panggilan layanan AI asinkron
- Mengubah HTML ke Markdown dengan Python - Pendekatan pengujian untuk pengambilan data web dan perpustakaan konversi konten
- Membuat PDF dalam Python - Strategi pengujian untuk pembuatan PDF dan output file
Pengujian Serverless dan Cloud
- Membangun AWS Lambda Ganda dengan Python dan Terraform - Strategi pengujian untuk fungsi Lambda, konsumen SQS, dan integrasi API Gateway
Pengujian unit adalah keterampilan penting bagi pengembang Python. Baik Anda memilih unittest atau pytest, kuncinya adalah menulis pengujian secara konsisten, menjaga agar mereka dapat dipelihara, dan mengintegrasikannya ke dalam alur kerja pengembangan Anda. Mulailah dengan pengujian sederhana, secara bertahap adopsi teknik lanjutan seperti penyamaran dan fixture, dan gunakan alat cakupan untuk mengidentifikasi kode yang belum diuji.