Unit Testing dengan pytest: Menulis Test yang Bersih dan Maintainable

Lhuqita Fazry
Python Programming pytest Testing
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:

pythonpython
!pip install pytest

pytest 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.

pythonpython
# 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 == 5

Output:

text
============================= 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.

pythonpython
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:

text
============================= test session starts ==============================
collected 2 items

test_fixtures.py ..                                                      [100%]

============================== 2 passed in 0.05s ==============================
Java Fundamental
Fundamental • Beginner

Java Fundamental

A hands-on, project-based introduction to Java programming designed for complete...

Daftar

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.

pythonpython
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 == expected

Output:

text
============================= 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.

pythonpython
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:

text
============================= 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.

text
project/
├── src/
│   ├── __init__.py
│   ├── calculator.py
│   └── utils.py
└── tests/
    ├── __init__.py
    ├── test_calculator.py
    ├── test_utils.py
    └── conftest.py

File 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.

pythonpython
# 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.

bashbash
# 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-missing

Option -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).

pythonpython
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 None

pytest 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

JavaCine: Terminal-Based Movie Ticketing System
Premium Course Fundamental

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.

Capstone Project

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.
7 Weeks Beginner
Lihat Detail Course
Personal Finance Tracker & Analyzer (CLI)
Premium Course Fundamental

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.

Capstone Project

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.
7 Weeks Beginner
Lihat Detail Course

Artikel Terkait