Test coverage mengukur berapa persen kode kamu yang benar-benar dijalankan waktu test suite berjalan. Bayangin satpam yang patroli malam dan menandatangani kartu absen di tiap ruangan. Kartu yang penuh tanda tangan membuktikan dia lewat semua ruangan, tapi gak membuktikan dia mengecek jendelanya sudah dikunci. Coverage itu kartu absennya: dia tahu baris mana yang dilewati test, tapi gak tahu apa yang dicek di sana.
🧪 Tebakan cepat: satu test,
hitungOngkir(150000, true), menjalankan semua baris function-nya, jadi line coverage-nya 100%. Branch coverage-nya berapa: 100%, 50%, atau 0%? Tebak dulu, jawabannya ada di bagian "Line coverage vs branch coverage".
Function yang mau diukur
Aturan ongkir sebuah toko: ongkirnya Rp20.000, member bayar setengahnya, dan belanja mulai Rp100.000 gratis ongkir.
// src/ongkir.js
export function hitungOngkir(total, member) {
let ongkir = 20000; // baris 2
if (member) { // baris 3
ongkir = ongkir / 2; // baris 4: member bayar setengah
}
if (total >= 100000) { // baris 6
ongkir = 0; // baris 7: mulai 100 ribu gratis
}
return ongkir; // baris 9
}
// src/ongkir.test.js
import { test, expect } from "vitest";
import { hitungOngkir } from "./ongkir.js";
test("member, belanja 150 ribu: gratis", () => {
expect(hitungOngkir(150000, true)).toBe(0);
});
Menjalankan coverage
npm install -D @vitest/coverage-v8
npx vitest --coverage
@vitest/coverage-v8adalah provider-nya, alat yang menghitung. Dia membaca catatan milik V8, mesin JavaScript di Node dan Chrome. Pilihan lainnya@vitest/coverage-istanbul, yang menyisipkan penghitung ke kodemu sebelum dijalankan. Laporannya sama bentuknya.npx vitest --coveragemenjalankan semua test sekaligus mengukur coverage. Di terminal biasa Vitest lalu menunggu perubahan file (watch mode).npx vitest run --coveragecuma jalan sekali, dan itu yang dipakai di CI.
Hasilnya untuk satu test tadi:
Test Files 1 passed (1)
Tests 1 passed (1)
% Coverage report from v8
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 50 | 100 | 100 |
ongkir.js | 100 | 50 | 100 | 100 | 3-6
-----------|---------|----------|---------|---------|-------------------
Empat angka di laporannya
- % Stmts (statement coverage): persen statement yang pernah jalan. Statement itu satu perintah, misalnya
let ongkir = 20000, sebuahif, ataureturn ongkir. File ini punya 6. - % Branch (branch coverage): persen cabang yang pernah dilewati. Tiap
ifpunya dua cabang: kondisinya true (masuk ke bloknya) dan false (melewati bloknya).else,switch, ternarya ? b : c, juga&&dan||juga bikin cabang. File ini punya 2if, jadi 4 cabang. - % Funcs (function coverage): persen function yang pernah dipanggil. Di sini 1 dari 1.
- % Lines (line coverage): persen baris berisi kode yang pernah jalan. Mirip Stmts, bedanya satu baris bisa berisi lebih dari satu statement.
- Uncovered Line #s: baris yang belum pernah jalan. Kalau semua baris sudah jalan, kolom ini menunjuk baris yang cabangnya belum lengkap.
3-6artinyaifdi baris 3 sampai 6.
Line coverage vs branch coverage
| Test yang dijalankan | % Lines | % Branch | Uncovered Line #s |
|---|---|---|---|
(150000, true) |
100 | 50 | 3-6 |
(50000, false) |
66,66 | 50 | 4,7 |
(150000, true) dan (50000, false) |
100 | 100 | |
(50000, true) dan (100000, false) |
100 | 100 |
- Test
(150000, true): member, jadi baris 4 jalan. Totalnya 150.000, jadi baris 7 juga jalan. Semua 6 baris kena: line coverage 100%. - Tapi dua cabang false gak pernah dilewati: "bukan member" dan "belanja di bawah 100 ribu". Cuma 2 dari 4 cabang, jadi branch coverage 50%. Itu jawaban tebakannya.
- Padahal cabang false itulah yang dilewati kebanyakan pembeli. Belum ada test yang membuktikan pembeli biasa bayar Rp20.000.
- Tambah test
(50000, false)dan keempat cabangnya terlewati: 100% dan 100%.
Makanya branch coverage lebih jujur daripada line coverage: line bisa 100% padahal separuh jalurnya belum pernah dicoba.
100% coverage bukan berarti bebas bug
Misalkan baris 6 salah ketik jadi if (total > 100000). Dua test tadi tetap hijau, dan coverage-nya tetap 100% untuk keempat angka. Padahal belanja tepat Rp100.000 harusnya gratis, dan function ini mengembalikan 20000. Bug-nya baru ketahuan kalau ada test tepat di batas:
test("tepat 100 ribu: gratis", () => {
expect(hitungOngkir(100000, false)).toBe(0);
});
FAIL src/ongkir.test.js > tepat 100 ribu: gratis
AssertionError: expected 20000 to be +0 // Object.is equality
Lebih parah lagi, test tanpa expect sama sekali juga dihitung. test("jalan", () => { hitungOngkir(150000, true); }) memberi line coverage 100% dan gak mengecek apa pun. Coverage cuma mencatat baris yang jalan. Dia gak tahu hasilnya dicek atau gak, dan gak tahu edge case seperti nilai batas, nol, atau data kosong sudah dicoba atau belum. Cara mengecek apakah test-mu benar-benar menangkap kesalahan ada di pelajaran berikutnya, mutation testing.
Threshold dan target yang masuk akal
// vitest.config.js
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "html"],
include: ["src/**/*.js"],
thresholds: {
lines: 80,
branches: 80,
functions: 80,
statements: 80,
},
},
},
});
reporter:textmencetak tabel tadi di terminal,htmlmembuat foldercoverage/. Bukacoverage/index.html: baris yang belum jalan berwarna merah, dan cabang yang belum dilewati diberi tandaI(jalur if belum pernah) atauE(jalur else belum pernah).include: file yang dihitung. Tanpa ini, file yang gak pernah di-import test gak muncul di laporan sama sekali, seolah aman. Denganinclude, file itu muncul dengan 0%.thresholds: batas minimal. Kalau kurang, Vitest mencetakERROR: Coverage for branches (50%) does not meet global threshold (80%)dan keluar dengan exit code 1, jadi pipeline CI ikut gagal.
Target yang umum: sekitar 80% untuk kode bisnis yang penting (harga, pembayaran, validasi), dengan perhatian lebih ke branch. Boilerplate, file konfigurasi, dan type definition boleh dikecualikan. Mengejar 100% biasanya berakhir dengan test asal jalan yang gak mengecek apa-apa.
⚠️ Jebakan yang sering ketemu
- Puas dengan line coverage 100%. Lihat kolom % Branch. Line 100% bisa berarti branch cuma 50%.
- Test tanpa
expect. Barisnya jalan, coverage naik, tapi gak ada yang dicek. - Gak mencoba nilai batas. Test 150.000 dan 50.000 gak pernah menyentuh tepat 100.000, satu-satunya tempat
>dan>=berbeda. - Menulis test cuma demi angka. Coverage itu peta bagian yang belum dites, bukan nilai rapor yang harus dikejar.
TL;DR: Test coverage mengukur kode yang dijalankan test: % Stmts, % Branch (cabang if/else, ternary, &&), % Funcs, dan % Lines. Jalankan dengan npx vitest --coverage (atau npx vitest run --coverage di CI) setelah memasang @vitest/coverage-v8. Line coverage 100% bisa berarti branch cuma 50%, jadi perhatikan branch. 100% pun gak menjamin bebas bug: test bisa melewati semua baris tanpa mencoba edge case, bahkan tanpa expect. Pasang thresholds supaya CI gagal kalau coverage turun, targetkan sekitar 80% di kode penting, dan pakai laporannya untuk menemukan bagian yang belum dites.