Unit Testing dengan pytest: Menulis Test yang Bersih dan Maintainable
Unit testing membantu kita memverifikasi bahwa code berfungsi sesuai harapan setiap kali ada perubahan. pytest menawarkan pendekatan yang lebih Pythonic dibandingkan unittest bawaan. Library ini menyederhanakan syntax test dan menyediakan fitur powerful seperti fixtures dan parametrization.
Mengapa pytest?
pytest menggunakan plain functions sebagai test cases, tidak memerlukan class inheritance. Test function yang ditulis dengan pytest lebih readable karena menggunakan plain assert statements. pytest secara otomatis menangkap assertion failures dan memberikan detailed output.
Keunggulan pytest meliputi:
- Simple syntax: Test functions menggunakan plain Python code tanpa boilerplate
- Rich plugin ecosystem: Plugins untuk coverage, mocking, dan integration dengan berbagai framework
- Detailed failure reports: Output yang informatif saat test gagal, termasuk context diff
- Fixture system: Dependency injection untuk setup dan teardown
Instalasi dan Struktur Test
Mulai dengan menginstall pytest:
!pip install pytestpytest secara default mendeteksi file yang namanya diawali dengan test_ atau diakhiri dengan _test.py. Di dalam file tersebut, function yang diawali dengan test_ akan dieksekusi sebagai test case.
# test_calculator.py
def add(a, b):
return a + b
def test_add_positive_numbers():
result = add(2, 3)
assert result == 5
def test_add_negative_numbers():
result = add(-1, -1)
assert result == -2
def test_add_zero():
result = add(0, 5)
assert result == 5Output:
============================= test session starts ==============================
collected 3 items
test_calculator.py ... [100%]
============================== 3 passed in 0.07s ===============================pytest menggunakan introspection untuk memberikan helpful error messages saat assertion gagal. Ketika assert result == 5 gagal, pytest akan menampilkan actual value dari result secara otomatis.
Fixtures untuk Setup dan Teardown
Fixture adalah function yang menyediakan test data atau resources. pytest menginject fixtures ke test functions melalui parameter.
import pytest
import tempfile
import os
@pytest.fixture
def sample_data():
return {
'users': [
{'id': 1, 'name': 'Alice', 'role': 'admin'},
{'id': 2, 'name': 'Bob', 'role': 'user'},
{'id': 3, 'name': 'Charlie', 'role': 'user'}
]
}
@pytest.fixture
def temp_file():
# Setup: create temporary file
fd, path = tempfile.mkstemp()
yield path
# Teardown: cleanup after test
os.close(fd)
os.unlink(path)
def test_find_admin(sample_data):
users = sample_data['users']
admin = next((u for u in users if u['role'] == 'admin'), None)
assert admin is not None
assert admin['name'] == 'Alice'
def test_file_operations(temp_file):
# Write to temporary file
with open(temp_file, 'w') as f:
f.write('test content')
# Read and verify
with open(temp_file, 'r') as f:
content = f.read()
assert content == 'test content'Output:
============================= test session starts ==============================
collected 2 items
test_fixtures.py .. [100%]
============================== 2 passed in 0.05s ==============================
Java Fundamental
A hands-on, project-based introduction to Java programming designed for complete...
Decorator @pytest.fixture menandai function sebagai fixture. yield statement memisahkan setup dan teardown code. Code sebelum yield dijalankan sebelum test, code setelah yield dijalankan setelah test selesai.
Parametrized Tests
Parametrization memungkinkan kita menjalankan test yang sama dengan berbagai input. Ini mengurangi duplication dan meningkatkan coverage.
import pytest
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
@pytest.mark.parametrize("number,expected", [
(2, True), # smallest prime
(3, True), # odd prime
(4, False), # even non-prime
(17, True), # larger prime
(1, False), # edge case
(0, False), # edge case
(-5, False), # negative number
])
def test_is_prime(number, expected):
result = is_prime(number)
assert result == expectedOutput:
============================= test session starts ==============================
collected 7 items
test_parametrize.py ....... [100%]
============================== 7 passed in 0.04s ==============================Decorator @pytest.mark.parametrize menerima string parameter names dan list of value tuples. pytest akan menjalankan test function sekali untuk setiap tuple. Setiap test case muncul sebagai separate test dalam output.
Assertion Patterns
pytest mendukung berbagai assertion patterns untuk berbagai scenarios.
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class TestDivisionOperations:
def test_divide_normal(self):
result = divide(10, 2)
assert result == 5.0
def test_divide_by_zero(self):
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "Cannot divide by zero" in str(exc_info.value)
def test_divide_result_type(self):
result = divide(10, 3)
assert isinstance(result, float)
assert result == pytest.approx(3.333, rel=1e-3)
def test_divide_collections(self):
results = [divide(10, n) for n in [2, 5, 10]]
assert results == [5.0, 2.0, 1.0]
assert all(isinstance(r, float) for r in results)Output:
============================= test session starts ==============================
collected 4 items
test_assertions.py .... [100%]
============================== 4 passed in 0.05s ==============================pytest.raises() digunakan untuk test exceptions. pytest.approx() membandingkan floating point numbers dengan tolerance. Assertions dapat digabungkan dalam satu test untuk verifikasi comprehensive.
Organisasi Test Files
Struktur directory yang baik memudahkan maintenance dan collaboration.
project/
├── src/
│ ├── __init__.py
│ ├── calculator.py
│ └── utils.py
└── tests/
├── __init__.py
├── test_calculator.py
├── test_utils.py
└── conftest.pyFile conftest.py menyimpan fixtures yang digunakan oleh multiple test files. Fixtures yang didefinisikan di conftest.py tersedia untuk semua test dalam directory tersebut dan subdirectories.
# conftest.py
import pytest
@pytest.fixture(scope="module")
def database_connection():
# Setup connection once per module
conn = create_db_connection()
yield conn
conn.close()
@pytest.fixture
def api_client():
from myapp import create_app
app = create_app(testing=True)
return app.test_client()Parameter scope="module" membuat fixture dijalankan sekali per test module, bukan sekali per test function. Ini meningkatkan performance saat setup operation expensive.
Running Tests
pytest menyediakan berbagai options untuk menjalankan test secara selective.
# Run all tests
pytest
# Run specific file
pytest tests/test_calculator.py
# Run specific test function
pytest tests/test_calculator.py::test_add_positive_numbers
# Run with verbose output
pytest -v
# Run tests matching pattern
pytest -k "prime"
# Stop on first failure
pytest -x
# Run with coverage report
pytest --cov=src --cov-report=term-missingOption -k menggunakan keyword expression untuk filter tests. Option -x (fail-fast) berguna saat debugging failures. Coverage report menunjukkan line mana yang belum dites.
Best Practices untuk Maintainable Tests
Beberapa prinsip untuk test suite yang sustainable:
1. Test names yang deskriptif: Nama test harus menjelaskan behavior yang diuji. Gunakan pattern test_[function]_[scenario]_[expected_result].
2. One assertion per concept: Setiap test sebaiknya fokus pada satu behavior. Multiple assertions acceptable jika mereka menguji aspek berbeda dari behavior yang sama.
3. Independent tests: Setiap test harus dapat dijalankan secara independen. Tidak boleh ada dependencies antar test.
4. Fast tests: Unit tests harus cepat. Integration tests yang lambat dipisahkan ke directory terpisah.
5. Arrange-Act-Assert: Struktur test dengan jelas: setup (arrange), execute (act), verify (assert).
def test_user_authentication_valid_credentials():
# Arrange
username = "alice"
password = "correct_password"
auth_service = AuthService()
# Act
result = auth_service.authenticate(username, password)
# Assert
assert result.success is True
assert result.user.username == username
assert result.token is not Nonepytest menyederhanakan proses testing dengan syntax yang clean dan fitur yang powerful. Dengan fixtures, parametrization, dan detailed reporting, kita dapat membangun test suite yang robust dan mudah dimaintain.
Mau belajar Python testing lebih dalam dengan hands-on project? Bergabunglah dengan Python Bootcamp di Rumah Coding. Kurikulum praktis dengan mentorship dari developer berpengalaman.
Course Terkait
Java Fundamental
A hands-on, project-based introduction to Java programming designed for complete beginners. Instead of merely memorizing syntax, you will learn to code by building real-world applications from day one. By the end of this course, you will master core programming logic, data structures, object-oriented principles, and debugging techniques, culminating in the development of a fully functional command-line system.
JavaCine: Terminal-Based Movie Ticketing System
- Object-Oriented Movie Catalog: Utilizes a Movie class to encapsulate details like title, genre, duration, and ticket price. The system displays a dynamic list of currently showing films.
- Dynamic Seat Visualization (2D Arrays): Uses a 2D Array to generate a visual seating grid (e.g., 5x5) in the terminal. Available seats are marked as [ O ] and booked seats are marked as [ X ].
- Interactive Booking Engine: A loop-driven menu that allows users to select a movie, choose a specific seat by row and column, and validates the choice. It prevents double-booking if a seat is already taken.
Python Fundamentals
Master the fundamentals of Python through hands-on, real-world projects. Designed for absolute beginners, this course takes you from writing your first line of code to building a fully functional application. By the end of this course, you will have a solid grasp of core programming concepts, data structures, and file management, laying a strong foundation for future studies in Data Science, Web Development, or Automation.
Personal Finance Tracker & Analyzer (CLI)
- Interactive Main Menu: A continuous loop menu allowing users to choose between adding records, viewing summaries, or exiting the app.
- Transaction Logging: Users can input transaction types (Income/Expense), amounts, categories (e.g., Food, Salary, Transport), and descriptions.
- Robust Input Validation: Utilizes try-except blocks to prevent the program from crashing if a user accidentally types letters instead of numbers for financial amounts.