Python

Python Testing dengan pytest

Tutorial lengkap belajar pytest dari nol โ€” fixtures, parametrize, markers, mocking, coverage, TDD, dan quiz interaktif dengan contoh kode praktis

1. Pengenalan Testing

Testing adalah proses memverifikasi bahwa kode berfungsi sesuai harapan. Dalam pengembangan perangkat lunak, testing membantu menemukan bug lebih awal, memastikan kode tidak rusak setelah perubahan (regression), dan memberikan dokumentasi tentang perilaku kode.

Jenis-jenis Testing

Jenis Deskripsi Contoh
Unit TestMenguji fungsi/komponen secara individualTest fungsi hitung_total()
Integration TestMenguji interaksi antar komponenTest API + Database
Functional TestMenguji fitur dari perspektif penggunaTest login flow
End-to-End TestMenguji seluruh alur aplikasiTest checkout dari keranjang

Mengapa pytest?

Keunggulan Penjelasan
Sintaks SederhanaCukup gunakan assert, tidak perlu method khusus
Auto-discoveryOtomatis menemukan file test yang dimulai dengan test_
FixturesSistem setup/teardown yang powerful dan reusable
ParametrizeJalankan test yang sama dengan data berbeda
Rich Plugin EkosistemRibuan plugin: coverage, HTML report, parallel, dll
Backward CompatibleBisa menjalankan test dari unittest dan nose
Diagram: Testing Pyramid
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  TESTING PYRAMID                      โ”‚
โ”‚                                                       โ”‚
โ”‚                    /\                                  โ”‚
โ”‚                   /  \                                 โ”‚
โ”‚                  / E2E \        โ† Sedikit, lambat,    โ”‚
โ”‚                 /________\         mahal               โ”‚
โ”‚                /          \                            โ”‚
โ”‚               / Integration\   โ† Sedang, menengah     โ”‚
โ”‚              /______________\                          โ”‚
โ”‚             /                \                         โ”‚
โ”‚            /    Unit Tests    \  โ† Banyak, cepat,     โ”‚
โ”‚           /____________________\     murah             โ”‚
โ”‚                                                       โ”‚
โ”‚  Unit: 70%  |  Integration: 20%  |  E2E: 10%         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

2. Memulai dengan pytest

Instalasi

Bash
# Instal pytest
pip install pytest

# Instal dengan plugin populer
pip install pytest pytest-cov pytest-html pytest-mock

# Verifikasi
pytest --version
# pytest 8.2.2

Test Pertama

Python โ€” kalkulator.py
# kalkulator.py โ€” Kode yang akan diuji
def tambah(a, b):
    return a + b

def kurang(a, b):
    return a - b

def kali(a, b):
    return a * b

def bagi(a, b):
    if b == 0:
        raise ValueError("Tidak bisa dibagi nol")
    return a / b

def rata_rata(angka_list):
    if not angka_list:
        raise ValueError("List tidak boleh kosong")
    return sum(angka_list) / len(angka_list)
Python โ€” test_kalkulator.py
# test_kalkulator.py โ€” File test
import pytest
from kalkulator import tambah, kurang, kali, bagi, rata_rata

# Test function โ€” diawali dengan "test_"
def test_tambah():
    assert tambah(2, 3) == 5
    assert tambah(-1, 1) == 0
    assert tambah(0, 0) == 0
    assert tambah(1.5, 2.5) == 4.0

def test_kurang():
    assert kurang(5, 3) == 2
    assert kurang(0, 5) == -5

def test_kali():
    assert kali(3, 4) == 12
    assert kali(-2, 3) == -6
    assert kali(0, 100) == 0

def test_bagi():
    assert bagi(10, 2) == 5.0
    assert bagi(7, 2) == 3.5

def test_bagi_dengan_nol():
    # Test bahwa error di-raise
    with pytest.raises(ValueError, match="Tidak bisa dibagi nol"):
        bagi(10, 0)

def test_rata_rata():
    assert rata_rata([1, 2, 3, 4, 5]) == 3.0
    assert rata_rata([10]) == 10.0

def test_rata_rata_kosong():
    with pytest.raises(ValueError, match="List tidak boleh kosong"):
        rata_rata([])

# Menjalankan test:
# pytest                    โ€” Jalankan semua test
# pytest test_kalkulator.py โ€” Jalankan file tertentu
# pytest -v                 โ€” Verbose output
# pytest -x                 โ€” Stop di test pertama yang gagal
# pytest -k "tambah"        โ€” Jalankan test yang mengandung "tambah"

Hasil Test

Terminal Output
$ pytest test_kalkulator.py -v
============================= test session starts =============================
collected 7 items

test_kalkulator.py::test_tambah         PASSED  [ 14%]
test_kalkulator.py::test_kurang         PASSED  [ 28%]
test_kalkulator.py::test_kali           PASSED  [ 42%]
test_kalkulator.py::test_bagi           PASSED  [ 57%]
test_kalkulator.py::test_bagi_dengan_nol PASSED [ 71%]
test_kalkulator.py::test_rata_rata      PASSED  [ 85%]
test_kalkulator.py::test_rata_rata_kosong PASSED [100%]

============================== 7 passed in 0.05s ==============================
๐Ÿ’ก Tips

Konvensi penamaan pytest: file test dimulai dengan test_ atau diakhiri dengan _test.py. Fungsi test dimulai dengan test_. Class test dimulai dengan Test. pytest akan otomatis menemukan dan menjalankan semua test yang sesuai konvensi.

3. Fixtures: Setup & Teardown

Fixtures adalah fungsi yang menyediakan data atau objek yang dibutuhkan oleh test. Fixtures digunakan untuk setup (persiapan) dan teardown (pembersihan) sebelum dan sesudah test berjalan. Ini sangat berguna untuk menghindari kode duplikat.

Python โ€” conftest.py
# conftest.py โ€” Fixtures yang bisa dipakai di semua test
import pytest
import tempfile
import os
import json

# Fixture sederhana
@pytest.fixture
def sample_data():
    """Menyediakan data sample untuk testing."""
    return {
        'nama': 'Budi',
        'umur': 25,
        'kota': 'Jakarta'
    }

# Fixture dengan yield (teardown)
@pytest.fixture
def temp_file():
    """Membuat file temporary, dihapus setelah test."""
    # Setup โ€” buat file
    filepath = tempfile.mktemp(suffix='.txt')
    with open(filepath, 'w') as f:
        f.write("Test data\n")

    yield filepath  # โ† Test berjalan di sini

    # Teardown โ€” hapus file
    if os.path.exists(filepath):
        os.remove(filepath)

# Fixture dengan scope
@pytest.fixture(scope='module')
def database_connection():
    """Koneksi database โ€” hanya dibuat sekali per module."""
    print("\n๐Ÿ“ก Membuka koneksi database...")
    connection = {'host': 'localhost', 'port': 5432, 'connected': True}
    yield connection
    print("\n๐Ÿ”Œ Menutup koneksi database...")
    connection['connected'] = False

# Fixture yang memanggil fixture lain
@pytest.fixture
def user_with_data(sample_data):
    """User yang sudah punya data."""
    return {
        'user': sample_data,
        'role': 'admin',
        'is_active': True
    }

# Fixture autouse โ€” otomatis dijalankan untuk semua test
@pytest.fixture(autouse=True)
def setup_test_environment():
    """Setup otomatis untuk setiap test."""
    print("\nโš™๏ธ Setup test environment...")
    yield
    print("\n๐Ÿงน Cleanup test environment...")

Menggunakan Fixtures

Python โ€” test_fixtures.py
# test_fixtures.py โ€” Menggunakan fixtures
import pytest

def test_sample_data(sample_data):
    """Menerima fixture sebagai parameter."""
    assert sample_data['nama'] == 'Budi'
    assert sample_data['umur'] == 25
    assert 'kota' in sample_data

def test_temp_file_exists(temp_file):
    """File temporary sudah dibuat sebelum test."""
    import os
    assert os.path.exists(temp_file)

    with open(temp_file, 'r') as f:
        content = f.read()
    assert content == "Test data\n"

def test_database_connection(database_connection):
    """Koneksi database tersedia."""
    assert database_connection['connected'] is True
    assert database_connection['host'] == 'localhost'

def test_user_role(user_with_data):
    """User dengan data sudah tersedia."""
    assert user_with_data['user']['nama'] == 'Budi'
    assert user_with_data['role'] == 'admin'
    assert user_with_data['is_active'] is True

# Fixture scope options:
# scope='function'  โ€” default, baru setiap test
# scope='class'     โ€” baru setiap class test
# scope='module'    โ€” baru setiap file test
# scope='package'   โ€” baru setiap package
# scope='session'   โ€” baru sekali untuk semua test

4. Parametrize: Test Cases Multiparameter

@pytest.mark.parametrize memungkinkan Anda menjalankan test yang sama dengan berbagai kombinasi input dan expected output. Ini mengurangi duplikasi kode test secara signifikan.

Python โ€” test_parametrize.py
import pytest
from kalkulator import tambah, kurang, kali, bagi

# Parametrize sederhana โ€” satu parameter
@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
    (1.5, 2.5, 4.0),
    (-5, -3, -8),
])
def test_tambah_parametrize(a, b, expected):
    assert tambah(a, b) == expected

# Parametrize untuk semua operasi
@pytest.mark.parametrize("op_func, a, b, expected", [
    (tambah, 10, 5, 15),
    (kurang, 10, 5, 5),
    (kali, 10, 5, 50),
    (bagi, 10, 5, 2.0),
])
def test_operasi_matematika(op_func, a, b, expected):
    assert op_func(a, b) == expected

# Parametrize untuk test error
@pytest.mark.parametrize("a, b", [
    (10, 0),
    (100, 0),
    (-5, 0),
])
def test_bagi_nol_parametrize(a, b):
    with pytest.raises(ValueError):
        bagi(a, b)

# Parametrize dengan ID custom (untuk output yang lebih jelas)
@pytest.mark.parametrize("input_str, expected", [
    pytest.param("hello", "HELLO", id="lowercase"),
    pytest.param("WORLD", "WORLD", id="uppercase"),
    pytest.param("Hello World", "HELLO WORLD", id="mixed"),
    pytest.param("123", "123", id="numbers"),
    pytest.param("", "", id="empty"),
], ids=str)
def test_string_upper(input_str, expected):
    assert input_str.upper() == expected

# Parametrize kombinasi (cartesian product)
@pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.parametrize("y", [10, 20])
def test_kombinasi(x, y):
    """Akan dijalankan 6 kali (3 x 2 kombinasi)."""
    assert x * y > 0

5. Markers: Mengelompokkan Test

Markers adalah decorator yang digunakan untuk memberikan metadata pada test. Marker memungkinkan Anda menjalankan sekelompok test tertentu, skip test, atau menandai test yang expected fail.

Python โ€” test_markers.py
import pytest
import sys

# Marker bawaan pytest

# @pytest.mark.skip โ€” Skip test
@pytest.mark.skip(reason="Belum diimplementasikan")
def test_fitur_baru():
    assert True

# @pytest.mark.skipif โ€” Skip kondisional
@pytest.mark.skipif(sys.platform == "win32", reason="Tidak jalan di Windows")
def test_linux_only():
    import os
    assert os.name == 'posix'

# @pytest.mark.xfail โ€” Expected failure
@pytest.mark.xfail(reason="Bug #123 belum diperbaiki")
def test_known_bug():
    assert 1 + 1 == 3  # Diharapkan gagal

# Custom markers โ€” didaftarkan di pytest.ini atau pyproject.toml
# [tool.pytest.ini_options]
# markers = [
#     "slow: tests that run slowly",
#     "integration: integration tests",
#     "smoke: smoke tests for quick validation",
# ]

@pytest.mark.slow
def test_komputasi_berat():
    """Test yang lambat, dijalankan hanya saat CI."""
    import time
    time.sleep(2)
    assert True

@pytest.mark.integration
def test_database_integration():
    """Test yang butuh database."""
    # Simulasi test database
    assert True

@pytest.mark.smoke
def test_aplikasi_berjalan():
    """Smoke test โ€” verifikasi dasar."""
    assert 1 + 1 == 2

# Menjalankan test berdasarkan marker:
# pytest -m slow            โ€” Jalankan test @slow
# pytest -m "not slow"      โ€” Skip test @slow
# pytest -m "integration or smoke" โ€” Jalankan keduanya
# pytest -m "not slow and not integration" โ€” Kecuali keduanya

Konfigurasi pytest

TOML โ€” pyproject.toml
# pyproject.toml โ€” Konfigurasi pytest
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v --tb=short"
markers = [
    "slow: Test yang berjalan lambat",
    "integration: Test integrasi dengan layanan eksternal",
    "smoke: Smoke test untuk validasi cepat",
]
filterwarnings = [
    "ignore::DeprecationWarning",
]

6. Mocking: Mengeksperimen Dependensi

Mocking adalah teknik mengganti dependensi eksternal (API, database, file system) dengan objek palsu (mock) yang perilakunya bisa dikontrol. Ini memungkinkan test berjalan cepat, terisolasi, dan tidak tergantung layanan eksternal.

Python โ€” kode yang akan di-mock
# services.py โ€” Kode yang menggunakan dependensi eksternal
import requests
from datetime import datetime

def get_cuaca(kota):
    """Mengambil data cuaca dari API eksternal."""
    response = requests.get(f"https://api.cuaca.com/{kota}")
    if response.status_code == 200:
        data = response.json()
        return {
            'kota': kota,
            'suhu': data['temperature'],
            'kondisi': data['condition']
        }
    return None

def get_waktu_server():
    """Mengambil waktu dari server."""
    return datetime.now()

def simpan_ke_database(data):
    """Menyimpan data ke database."""
    # Simulasi database write
    from database import db
    db.insert(data)
    return True

def proses_pesanan(pesanan):
    """Proses pesanan dengan validasi stok dan pembayaran."""
    stok = get_stok_barang(pesanan['barang_id'])
    if stok < pesanan['jumlah']:
        return {'status': 'gagal', 'alasan': 'Stok tidak cukup'}

    hasil_bayar = proses_pembayaran(pesanan['total'])
    if not hasil_bayar['sukses']:
        return {'status': 'gagal', 'alasan': 'Pembayaran gagal'}

    return {'status': 'berhasil', 'order_id': hasil_bayar['order_id']}
Python โ€” test_mocking.py
# test_mocking.py โ€” Menggunakan mock
import pytest
from unittest.mock import patch, MagicMock, Mock
from services import get_cuaca, get_waktu_server, proses_pesanan

# Mock dengan @patch decorator
@patch('services.requests.get')
def test_get_cuaca_berhasil(mock_get):
    """Test get_cuaca dengan mock API response."""
    # Setup mock response
    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        'temperature': 28,
        'condition': 'Cerah'
    }
    mock_get.return_value = mock_response

    # Jalankan fungsi
    hasil = get_cuaca('Jakarta')

    # Verifikasi
    assert hasil['kota'] == 'Jakarta'
    assert hasil['suhu'] == 28
    assert hasil['kondisi'] == 'Cerah'

    # Verifikasi mock dipanggil dengan benar
    mock_get.assert_called_once_with('https://api.cuaca.com/Jakarta')

@patch('services.requests.get')
def test_get_cuaca_gagal(mock_get):
    """Test get_cuaca saat API gagal."""
    mock_response = MagicMock()
    mock_response.status_code = 500
    mock_get.return_value = mock_response

    hasil = get_cuaca('Jakarta')
    assert hasil is None

# Mock dengan context manager
def test_get_waktu():
    """Test get_waktu dengan mock waktu."""
    with patch('services.datetime') as mock_datetime:
        mock_datetime.now.return_value = datetime(2026, 6, 25, 10, 30)
        waktu = get_waktu_server()
        assert waktu.year == 2026
        assert waktu.month == 6

# Mock dengan patch.object
def test_proses_pesanan_stok_kurang():
    """Test pesanan gagal karena stok kurang."""
    with patch('services.get_stok_barang', return_value=5):
        with patch('services.proses_pembayaran') as mock_bayar:
            pesanan = {'barang_id': 1, 'jumlah': 10, 'total': 500000}
            hasil = proses_pesanan(pesanan)

            assert hasil['status'] == 'gagal'
            assert 'Stok tidak cukup' in hasil['alasan']
            mock_bayar.assert_not_called()  # Pembayaran tidak dipanggil

# Mock dengan side_effect (simulasi error)
@patch('services.requests.get')
def test_api_timeout(mock_get):
    """Test saat API timeout."""
    mock_get.side_effect = TimeoutError("Connection timeout")

    with pytest.raises(TimeoutError):
        get_cuaca('Jakarta')
โš ๏ธ Perhatian Saat Mocking

Saat menggunakan @patch, patch target harus di tempat modul menggunakannya, bukan di tempat modul didefinisikan. Misalnya: jika services.py mengimpor requests, maka patch 'services.requests.get', bukan 'requests.get'.

7. Test Coverage

Test coverage mengukur seberapa besar persentase kode yang teruji oleh test. Coverage membantu menemukan bagian kode yang belum memiliki test.

Bash
# Instal pytest-cov
pip install pytest-cov

# Jalankan test dengan coverage
pytest --cov=. --cov-report=term-missing

# Output:
# ---------- coverage: platform linux, python 3.12 ----------
# Name                Stmts   Miss  Cover   Missing
# -------------------------------------------------
# kalkulator.py          12      1    92%   11
# services.py            20      5    75%   25-30
# -------------------------------------------------
# TOTAL                  32      6    81%

# Generate HTML report
pytest --cov=. --cov-report=html
# Buka htmlcov/index.html di browser

# Set minimum coverage (gagal jika di bawah 80%)
pytest --cov=. --cov-fail-under=80

# Coverage untuk file tertentu
pytest --cov=kalkulator --cov-report=term-missing

Konfigurasi Coverage

TOML โ€” .coveragerc atau pyproject.toml
# pyproject.toml
[tool.coverage.run]
source = ["."]
omit = [
    "tests/*",
    "venv/*",
    "*/migrations/*",
    "conftest.py",
]

[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = [
    "pragma: no cover",
    "if __name__ == .__main__.",
    "if TYPE_CHECKING:",
    "raise NotImplementedError",
]

8. Test-Driven Development (TDD)

TDD adalah metodologi pengembangan di mana Anda menulis test sebelum menulis kode produksi. Siklus TDD dikenal sebagai Red-Green-Refactor.

Diagram: Siklus TDD
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                SIKLUS TDD                             โ”‚
โ”‚                                                       โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                       โ”‚
โ”‚    โ”‚ 1. RED   โ”‚  Tulis test yang GAGAL               โ”‚
โ”‚    โ”‚ (Gagal)  โ”‚  (kode belum ada)                    โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜                                       โ”‚
โ”‚         โ”‚                                             โ”‚
โ”‚         โ–ผ                                             โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                       โ”‚
โ”‚    โ”‚ 2. GREEN โ”‚  Tulis kode MINIMAL agar test lulus   โ”‚
โ”‚    โ”‚ (Lulus)  โ”‚  (tidak lebih, tidak kurang)          โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜                                       โ”‚
โ”‚         โ”‚                                             โ”‚
โ”‚         โ–ผ                                             โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                       โ”‚
โ”‚    โ”‚ 3. REFA- โ”‚  Perbaiki struktur kode               โ”‚
โ”‚    โ”‚   CTOR   โ”‚  (pastikan test masih lulus)          โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜                                       โ”‚
โ”‚         โ”‚                                             โ”‚
โ”‚         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Ulangi siklus                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Contoh TDD: Membuat Class Keranjang Belanja

Python โ€” Step 1: RED โ€” Tulis test
# test_keranjang.py โ€” Tulis test DULU (akan gagal karena class belum ada)
import pytest
from keranjang import Keranjang, Produk

class TestKeranjang:
    @pytest.fixture
    def keranjang(self):
        return Keranjang()

    @pytest.fixture
    def produk_laptop(self):
        return Produk(id=1, nama="Laptop", harga=10000000)

    @pytest.fixture
    def produk_mouse(self):
        return Produk(id=2, nama="Mouse", harga=150000)

    def test_keranjang_awalnya_kosong(self, keranjang):
        assert keranjang.jumlah_item() == 0
        assert keranjang.total() == 0

    def test_tambah_produk(self, keranjang, produk_laptop):
        keranjang.tambah(produk_laptop)
        assert keranjang.jumlah_item() == 1

    def test_tambah_produk_dengan_jumlah(self, keranjang, produk_mouse):
        keranjang.tambah(produk_mouse, jumlah=3)
        assert keranjang.jumlah_item() == 3

    def test_total_harga(self, keranjang, produk_laptop, produk_mouse):
        keranjang.tambah(produk_laptop)
        keranjang.tambah(produk_mouse, jumlah=2)
        assert keranjang.total() == 10000000 + (150000 * 2)

    def test_hapus_produk(self, keranjang, produk_laptop):
        keranjang.tambah(produk_laptop)
        keranjang.hapus(produk_laptop.id)
        assert keranjang.jumlah_item() == 0

    def test_hapus_produk_tidak_ada(self, keranjang):
        with pytest.raises(ValueError, match="Produk tidak ditemukan"):
            keranjang.hapus(999)

    def test_produk_string(self, produk_laptop):
        assert str(produk_laptop) == "Laptop (Rp 10,000,000)"
Python โ€” Step 2: GREEN โ€” Tulis kode
# keranjang.py โ€” Implementasi agar test lulus
class Produk:
    def __init__(self, id, nama, harga):
        self.id = id
        self.nama = nama
        self.harga = harga

    def __str__(self):
        return f"{self.nama} (Rp {self.harga:,.0f})"

class Keranjang:
    def __init__(self):
        self._items = {}  # {produk_id: {'produk': ..., 'jumlah': ...}}

    def tambah(self, produk, jumlah=1):
        if produk.id in self._items:
            self._items[produk.id]['jumlah'] += jumlah
        else:
            self._items[produk.id] = {'produk': produk, 'jumlah': jumlah}

    def hapus(self, produk_id):
        if produk_id not in self._items:
            raise ValueError("Produk tidak ditemukan")
        del self._items[produk_id]

    def jumlah_item(self):
        return sum(item['jumlah'] for item in self._items.values())

    def total(self):
        return sum(
            item['produk'].harga * item['jumlah']
            for item in self._items.values()
        )

# Menjalankan:
# pytest test_keranjang.py -v
# Semua 7 test seharusnya PASSED โœ…

9. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang pytest:

Pertanyaan 1: Bagaimana cara pytest menemukan file test?

a) File yang diakhiri dengan .test.py
b) File yang dimulai dengan test_ atau diakhiri dengan _test.py
c) File yang ada di folder tests/ saja
d) File yang mengimpor pytest

Pertanyaan 2: Apa fungsi dari fixture dengan yield?

a) Mengembalikan beberapa nilai sekaligus
b) Memisahkan setup (sebelum yield) dan teardown (setelah yield)
c) Membuat fixture berjalan lebih cepat
d) Menghindari error saat test gagal

Pertanyaan 3: Apa yang dilakukan decorator @pytest.mark.parametrize?

a) Mengelompokkan test berdasarkan kategori
b) Menjalankan test yang sama dengan beberapa set data input
c) Menandai test yang diharapkan gagal
d) Menjalankan test secara paralel

Pertanyaan 4: Apa itu mocking dalam testing?

a) Menghapus test yang tidak diperlukan
b) Mengganti dependensi eksternal dengan objek palsu yang perilakunya dikontrol
c) Menulis test tanpa assert
d) Membuat test berjalan lebih lambat

Pertanyaan 5: Apa urutan siklus dalam TDD?

a) Write Code โ†’ Write Test โ†’ Refactor
b) Red โ†’ Green โ†’ Refactor
c) Design โ†’ Implement โ†’ Test
d) Plan โ†’ Code โ†’ Deploy
๐Ÿ” Zoom
100%
๐ŸŽจ Tema