Cakupan kode (code coverage) adalah ukuran proporsi kode yang benar-benar dieksekusi saat pengujian berjalan. Topik ini membahas cara mengukur cakupan kode pada proyek .NET menggunakan coverlet, cara membaca laporan yang dihasilkan, perbedaan antara angka yang tinggi dan cakupan yang bermakna, serta penerapan ambang batas sebagai gerbang kualitas di pipeline CI. Setelah topik ini dipelajari, angka cakupan dapat dibaca secara kritis dan kegagalan gerbang kualitas pada CI dapat ditafsirkan dengan tepat.
Cakupan baris dan cakupan cabang#
Cakupan baris (line coverage) menghitung proporsi baris kode yang dieksekusi oleh pengujian, sedangkan cakupan cabang (branch coverage) menghitung proporsi jalur keputusan yang dilalui. Setiap pernyataan if memuat sedikitnya dua jalur, yaitu kondisi yang bernilai true dan false. Pengujian yang hanya melewati satu sisi membuat jalur lainnya tidak tercatat meskipun baris pernyataannya sendiri tereksekusi. Karena alasan itulah kedua angka dilaporkan secara terpisah.
Proporsi keduanya dinyatakan dalam persen menurut rumus berikut.
Sebagai gambaran, sebuah metode yang memeriksa apakah masukan kosong memiliki dua jalur keputusan. Pengujian yang hanya mengirim masukan kosong mencakup satu dari dua cabang, sehingga cakupan cabangnya 50% meskipun baris yang tereksekusi terlihat banyak. Baris yang tercatat pada laporan bisa lebih banyak daripada cabangnya, sehingga cakupan baris 100% belum tentu disertai cakupan cabang 100%. Cabang yang tidak terjangkau berpotensi menyimpan cacat yang tidak pernah ditemukan, dan inilah kelemahan yang ingin ditangkap oleh cakupan cabang.
Mengukur cakupan dengan coverlet#
coverlet adalah alat pengukur cakupan kode untuk .NET yang bekerja dengan mencatat setiap baris dan cabang yang dieksekusi selama pengujian berjalan. Templat dotnet new xunit sudah merujuk package coverlet.collector, sehingga pengukuran dapat dilakukan tanpa menambah package apa pun. Kolektor ini dipanggil melalui opsi --collect:"XPlat Code Coverage" pada perintah dotnet test, dan laporannya ditulis setelah seluruh pengujian selesai.
Kelas yang diuji adalah PenghitungKata, yaitu kelas statis yang menghitung jumlah kata pada sebuah kalimat. Metode Hitung memuat beberapa jalur keputusan, mulai dari masukan null dan spasi kosong hingga spasi berlebih di antara kata. Kelas semacam ini cukup kecil untuk diperiksa mata, tetapi jalurnya cukup banyak untuk menghasilkan laporan cakupan yang berarti.
// Kelas yang diuji: penghitung kata pada sebuah kalimat.
public static class PenghitungKata
{
public static int Hitung(string kalimat)
{
if (string.IsNullOrWhiteSpace(kalimat))
{
return 0;
}
int jumlah = 0;
bool dalamKata = false;
foreach (char c in kalimat)
{
if (char.IsWhiteSpace(c))
{
dalamKata = false;
}
else if (!dalamKata)
{
dalamKata = true;
jumlah++;
}
}
return jumlah;
}
}Berkas pengujian memuat empat metode Fact yang mencakup seluruh jalur tersebut, termasuk kasus masukan null dan spasi berlebih. Setiap metode memuat satu skenario dengan assertion yang menyatakan hasil yang diharapkan. Pernyataan null! pada pengujian pertama menyatakan bahwa peringatan nullable sengaja diabaikan karena masukan null memang yang diuji.
using Xunit;
public class PenghitungKataTests
{
[Fact]
public void Hitung_KalimatNull_MengembalikanNol()
{
Assert.Equal(0, PenghitungKata.Hitung(null!));
}
[Fact]
public void Hitung_SpasiSaja_MengembalikanNol()
{
Assert.Equal(0, PenghitungKata.Hitung(" "));
}
[Fact]
public void Hitung_KalimatBiasa_MenghitungJumlahKata()
{
Assert.Equal(3, PenghitungKata.Hitung("kucing tidur nyenyak"));
}
[Fact]
public void Hitung_SpasiBerlebih_MenghitungKataTanpaDuplikasi()
{
Assert.Equal(2, PenghitungKata.Hitung(" kucing lapar "));
}
}Proyek uji dibuat pada folder kerja kosong, lalu kedua berkas di atas disalin ke dalamnya dan contoh bawaan templat dihapus.
# membuat proyek uji xUnit di dalam folder kerja
dotnet new xunit -o UjiCakupan 2>&1 | grep "Restore succeeded"Restore succeeded.# menyalin kelas yang diuji dan berkas pengujian, contoh bawaan templat dihapus
cp PenghitungKata.cs PenghitungKataTests.cs UjiCakupan/
rm UjiCakupan/UnitTest1.cs# menjalankan seluruh pengujian, hanya baris ringkasan yang ditampilkan
dotnet test UjiCakupan 2>&1 | grep -E "Passed!|Failed!" | sed 's/, Duration.*//'Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4Membaca laporan cakupan#
Pengukuran menghasilkan laporan dalam format Cobertura, yaitu format XML yang umum dipakai alat cakupan kode. Selain ringkasan pengujian, perintah --collect mencetak baris Attachments: yang berisi jalur laporan bernama coverage.cobertura.xml. Jalur itu memuat pengenal sesi acak (GUID) yang berubah pada setiap eksekusi, sehingga folder TestResults menampung satu folder laporan baru untuk setiap eksekusi.
# mengukur cakupan kode dengan kolektor coverlet saat pengujian berjalan
dotnet test UjiCakupan --collect:"XPlat Code Coverage" 2>&1 | grep -E "Passed!|Failed!" | sed 's/, Duration.*//'Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4Angka pada laporan dibaca dari atribut XML-nya. Agar angkanya dapat dibandingkan secara pasti, contoh berikut menggunakan dotnet-coverage, yaitu alat resmi Microsoft berbasis profiler yang menulis laporan Cobertura dengan atribut akar yang sama. Alat ini dipasang sebagai global tool, kemudian menjalankan pengujian di bawah pengamatannya.
# memasang alat dotnet-coverage bila belum ada, lalu mendaftarkan folder global tool
dotnet tool list --global | grep -q dotnet-coverage || dotnet tool install --global dotnet-coverage
export PATH="$PATH:$HOME/.dotnet/tools"# menjalankan pengujian di bawah profiler dotnet-coverage, laporan ditulis ke jalur tetap
dotnet-coverage collect -f cobertura -o UjiCakupan/TestResults/laporan.cobertura.xml dotnet test UjiCakupan 2>&1 | grep -E "Passed!|Failed!" | sed 's/, Duration.*//'Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4# membaca jumlah baris dan cabang yang tercakup dari laporan
grep -oE 'lines-covered="[0-9]+" lines-valid="[0-9]+"' UjiCakupan/TestResults/laporan.cobertura.xml
grep -oE 'branches-covered="[0-9]+" branches-valid="[0-9]+"' UjiCakupan/TestResults/laporan.cobertura.xmllines-covered="32" lines-valid="32"
branches-covered="8" branches-valid="8"Laporan tersebut mencatat 32 dari 32 baris dan 8 dari 8 cabang yang tercakup, sehingga cakupan baris dan cakupan cabangnya masing-masing 100%. Bentuk pecahan dari persentase itu tampak pada atribut line-rate dan branch-rate yang bernilai 1. Apabila ada baris atau cabang yang tidak terjangkau, nilainya menjadi pecahan di bawah satu, misalnya 0.75 untuk tiga perempat bagian yang tercakup. Atribut tersebut ada pada elemen akar dan setiap elemen kelas atau metode, seperti potongan berikut.
<class line-rate="1" branch-rate="1" complexity="8" name="PenghitungKata">
<methods>
<method line-rate="1" branch-rate="1" complexity="8" name="Hitung" signature="(string)">
...
</method>
</methods>
</class># menghapus proyek uji sementara setelah latihan selesai
rm -rf UjiCakupanCakupan yang bermakna#
Angka cakupan yang tinggi bukan tujuan akhir pengujian. Cakupan hanya menyatakan baris yang dieksekusi, bukan baris yang diperiksa hasilnya. Pengujian yang memanggil metode tanpa satu pun assertion tetap menaikkan cakupan, tetapi tidak menangkap kesalahan apa pun karena tidak ada pemeriksaan yang bisa gagal.
// tidak-dijalankan
[Fact]
public void Hitung_KalimatBiasa_TanpaAsersi()
{
// baris metode tereksekusi sehingga tercatat pada laporan cakupan
PenghitungKata.Hitung("kucing tidur nyenyak");
}Cakupan yang bermakna tumbuh dari assertion yang memeriksa perilaku, bukan dari usaha mengejar angka. Setiap pengujian perlu menyatakan harapan yang jelas, seperti jumlah kata yang benar atau pelemparan exception pada masukan kosong. Cakupan kemudian berfungsi menunjukkan jalur mana yang belum tersentuh pengujian mana pun. Baris yang tetap kosong setelah perilakunya diuji biasanya menandakan kode yang tidak terpakai dan layak dihapus.
Gerbang kualitas di CI#
Gerbang kualitas (quality gate) adalah aturan yang menggagalkan perubahan bila syarat tertentu tidak terpenuhi. Pada pipeline CI, syarat yang paling dasar adalah seluruh pengujian lulus, dan syarat itu diwujudkan oleh satu langkah dotnet test. Perintah yang gagal keluar dengan kode selain nol, sehingga langkah berikutnya tidak dijalankan dan build dinyatakan gagal.
Ambang batas (threshold) cakupan ditegakkan melalui package coverlet.msbuild, yang menambahkan properti MSBuild pada perintah dotnet test. Properti Threshold menetapkan persentase minimum, sedangkan ThresholdType menentukan ukurannya, misalnya cakupan baris. Apabila hasil berada di bawah ambang, perintah keluar dengan kode selain nol sehingga langkah pipeline yang bersangkutan gagal. Pipeline berikut, yang ditulis untuk GitHub Actions sebagai contoh umum, menjalankan pengujian dengan ambang batas lalu menyimpan laporannya sebagai artifact.
name: uji-kualitas
on: [push]
jobs:
uji:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Jalankan pengujian dengan ambang cakupan
run: >
dotnet test
/p:CollectCoverage=true
/p:CoverletOutputFormat=cobertura
/p:Threshold=80
/p:ThresholdType=line
- name: Simpan laporan cakupan
if: always()
uses: actions/upload-artifact@v4
with:
name: laporan-cakupan
path: '**/coverage.cobertura.xml'Nilai ambang perlu dipilih dengan hati-hati. Ambang yang terlalu tinggi pada kode lama membuat hampir setiap perubahan gagal, sedangkan ambang yang terlalu rendah tidak menghalangi apa pun. Nilai yang lazim dipilih sedikit di atas cakupan saat ini, lalu dinaikkan bertahap seiring bertambahnya pengujian. Laporan Cobertura yang disimpan sebagai artifact memudahkan penelusuran baris yang menyebabkan kegagalan gerbang.
Kesalahan umum#
Menetapkan ambang batas melalui opsi --collect tidak berfungsi. Kolektor XPlat Code Coverage tidak menyediakan opsi ambang, sehingga pengukuran tetap berjalan dan laporan tetap dihasilkan, tetapi tidak ada pemeriksaan minimum yang dilakukan. Perbaikannya adalah menambahkan package coverlet.msbuild ke proyek uji, lalu menegakkan ambang melalui properti Threshold sebagaimana contoh pipeline di atas.
Membaca laporan dengan pola glob pada folder TestResults sering memuat berkas yang salah. Setiap eksekusi --collect membuat folder GUID baru, sehingga laporan lama menumpuk dan pencarian bisa menemukan beberapa laporan sekaligus. Perbaikannya adalah menghapus folder TestResults sebelum eksekusi atau menunjuk berkas laporan secara eksplisit, seperti nama tetap laporan.cobertura.xml pada contoh pembacaan angka.
Menafsirkan cakupan penuh sebagai jaminan bebas dari bug juga merupakan kekeliruan yang lazim. Baris yang tereksekusi belum tentu diperiksa assertionnya, dan kombinasi masukan yang tidak terduga tetap bisa lolos dari jalur yang sudah tercakup. Cakupan menunjukkan kode yang belum diuji, sedangkan penilaian kebenarannya tetap bergantung pada mutu assertion.
Ringkasan#
- Cakupan baris menghitung baris yang dieksekusi, sedangkan cakupan cabang menghitung jalur keputusan yang dilalui, dan keduanya dilaporkan terpisah.
- Perintah
dotnet test --collect:"XPlat Code Coverage"menjalankan pengujian dengan kolektorcoverlet.collectordan menulis laporan Cobertura di dalamTestResults. - Angka cakupan dibaca dari atribut laporan, misalnya
lines-covereddanlines-valid, dan bermakna hanya bila setiap pengujian memuat assertion yang memeriksa perilaku. - Gerbang kualitas di CI ditegakkan dengan langkah
dotnet testditambah ambang batasThresholddaricoverlet.msbuild, yang menggagalkan build bila cakupan di bawah nilai minimum.
Referensi#
- Use code coverage for unit testing — Microsoft Learn — Panduan resmi pengukuran cakupan kode pada pengujian .NET menggunakan coverlet dan dotnet-coverage.
- Coverlet integration with MSBuild — coverlet-coverage
— Dokumentasi properti
Thresholduntuk menegakkan ambang batas cakupan padadotnet test. - Testing with ‘dotnet test’ — Microsoft Learn
— Panduan resmi menjalankan pengujian .NET dengan perintah
dotnet test.