Beranda / Kursus / Testing for Beginners / II — Praktik
II — Praktik
Artikel 7 dari 9

Praktik: Test Coverage & Batasannya

Mengukur coverage dengan pytest-cov, kenapa kode yang di-bypass lewat dependency override muncul sebagai tidak tercakup, kenapa 100% coverage bukan tujuan, dan sekilas soal mutation testing.

10 Juli 2026

Kita sudah punya banyak test — unit, integration, parametrized, dengan mocking untuk dependency eksternal. Pertanyaan alami berikutnya: seberapa banyak dari kode kita yang benar-benar tercakup test? Di artikel ini kita ukur dengan test coverage, sekaligus membahas jujur apa yang angka ini TIDAK bisa katakan — supaya tidak terjebak mengejar angka yang salah.

Mengukur Coverage dengan pytest-cov

pip install pytest-cov
pytest --cov=app --cov-report=term-missing

Hasilnya, dijalankan sungguhan di task-tracker-api:

Name                      Stmts   Miss  Cover   Missing
-------------------------------------------------------
app/__init__.py               0      0   100%
app/config.py                 16      0   100%
app/database.py               12      6    50%   15-18, 22-23
app/dependencies.py           16      1    94%   28
app/logging_config.py         17      1    94%   17
app/main.py                   29      2    93%   18-19
app/models.py                 20      0   100%
app/notifications.py           8      2    75%   23-24
app/rate_limit.py             14      1    93%   10
app/routers/auth.py           26      0   100%
app/routers/tasks.py          54      3    94%   23, 107-108
app/schemas.py                29      0   100%
app/security.py               21      0   100%
-------------------------------------------------------
TOTAL                        262     16    94%

94% total — kelihatannya bagus. Tapi mari kita bongkar baris app/database.py yang cuma 50%, karena di situlah pelajaran paling pentingnya.

Membaca --cov-report=term-missing

Kolom Missing menunjukkan NOMOR BARIS yang tidak pernah dieksekusi test mana pun. Untuk app/database.py, itu baris 15-18 dan 22-23:

def create_db_and_tables() -> None:      # baris 14
    try:                                    # baris 15 — TIDAK tercakup
        SQLModel.metadata.create_all(engine)   # baris 16 — TIDAK tercakup
    except (ProgrammingError, IntegrityError):  # baris 17 — TIDAK tercakup
        pass                                    # baris 18 — TIDAK tercakup


def get_session():                        # baris 21
    with Session(engine) as session:        # baris 22 — TIDAK tercakup
        yield session                          # baris 23 — TIDAK tercakup

Kenapa fungsi-fungsi ini tidak tercakup, padahal jelas-jelas ada di kode yang jalan di production? Karena di artikel sebelumnya, kita OVERRIDE get_session dengan versi test lewat app.dependency_overrides — implementasi ASLI fungsi ini TIDAK PERNAH benar-benar dipanggil selama test berjalan, karena FastAPI selalu memakai versi override-nya.

Pelajaran Inti: Angka Tinggi Bisa Menyembunyikan Kode yang Sama Sekali Tidak Teruji

Ini poin paling penting di artikel ini: 94% coverage BUKAN berarti 94% dari PERILAKU aplikasi sudah teruji. Coverage cuma mengukur baris kode mana yang DIEKSEKUSI selama test berjalan — tidak peduli apakah hasilnya benar-benar DIPERIKSA (assert) atau tidak, dan sama sekali tidak tahu soal kode yang sengaja "dilewati" lewat mocking atau dependency override, seperti kasus database.py di atas.

Implikasinya: kalau create_db_and_tables() punya bug (misalnya salah nama exception yang di-catch), coverage report kita TIDAK AKAN PERNAH memberi tahu itu — karena fungsinya memang tidak pernah benar-benar dijalankan test mana pun. Ini bukan cacat di test suite kita (dependency override itu sendiri tujuannya justru BENAR — kita memang tidak mau test menyentuh Postgres sungguhan), tapi pengingat bahwa fungsi seperti ini butuh jenis verifikasi LAIN — misalnya, dicoba manual sekali saat setup awal project (seperti yang kita lakukan di seri-seri sebelumnya saat memverifikasi docker compose up benar-benar berhasil membuat tabel).

Kenapa 100% Coverage Bukan Tujuan

Mengejar 100% coverage sebagai target itu jebakan umum. Dua alasan:

  • Coverage tinggi bisa dicapai dengan test yang BURUK. Test yang memanggil fungsi tapi tidak pernah assert apa pun tetap menaikkan angka coverage, walaupun tidak membuktikan apa-apa. Angka itu sendiri tidak membedakan test yang benar-benar memverifikasi perilaku dari test yang cuma "menyentuh" kode.
  • Sebagian kode memang tidak sepadan untuk dites secara unit/integration. Baris konfigurasi trivial, __repr__ untuk debugging, atau kode yang cuma jalan di kondisi environment yang sangat spesifik — usaha untuk memaksa 100% di situ sering kali menghasilkan test yang rapuh dan mahal dipelihara, tanpa manfaat proporsional.
  • Target yang lebih sehat: fokus pada BUSINESS LOGIC dan jalur kritis (autentikasi, otorisasi, validasi, kalkulasi penting) — persis yang sudah kita bangun sejak artikel-artikel sebelumnya. Coverage yang tinggi di area itu jauh lebih berharga daripada coverage tinggi merata di semua file tanpa pandang penting-tidaknya.

    Sekilas: Mutation Testing — Melangkah Lebih Jauh dari Coverage

    Kalau coverage cuma bilang "baris ini dieksekusi", ada teknik yang lebih ketat: mutation testing. Tool-nya (misalnya mutmut untuk Python) sengaja MERUSAK kode kita sedikit demi sedikit (mengubah == jadi !=, > jadi <, dst.) lalu menjalankan test suite — kalau test tetap LOLOS walau kodenya sudah dirusak, itu tanda test-nya tidak benar-benar memeriksa perilaku itu, walaupun coverage-nya bilang 100%. Ini di luar cakupan seri dasar ini, tapi baik untuk diketahui sebagai langkah lanjutan (juga disebut di roadmap artikel penutup seri ini) begitu test suite sudah cukup matang.

    Rangkuman

  • pytest-cov mengukur baris kode mana yang dieksekusi selama test berjalan, bukan seberapa BENAR perilakunya diverifikasi.
  • Kode yang di-bypass lewat dependency override (seperti get_session asli) akan MUNCUL sebagai tidak tercakup — ini bukan bug, tapi konsekuensi wajar dari cara kita mengisolasi test.
  • 100% coverage bukan tujuan — angka tinggi bisa dicapai dengan test yang tidak benar-benar memverifikasi apa pun; fokus ke business logic dan jalur kritis.
  • Mutation testing melangkah lebih jauh dari coverage biasa, memverifikasi apakah test benar-benar "menangkap" perubahan perilaku, bukan cuma mengeksekusi baris kode.
  • Topik

    TestingCoverage