Kita dapat menjalankan ratusan test, tetapi tanpa pengukuran tidak ada cara untuk mengetahui baris mana yang belum tersentuh. Code coverage adalah metrik yang menjawab pertanyaan “Berapa persen baris, cabang, atau fungsi yang dijalankan oleh test suite?”. Pada chapter ini kita menggunakan gcov—alat coverage bawaan GCC—untuk mengukur cakupan test pada modul grade_letter(). Kita juga melihat output file .gcov yang menunjukkan secara eksplisit baris mana yang belum diuji, lalu memperbaiki test hingga mencapai 100% line coverage.

Mengaktifkan Coverage dengan GCC #

GCC menyediakan dua flag untuk instrumentasi coverage:

  • -fprofile-arcs: Menambahkan kode yang mencatat eksekusi setiap basic block ke file .gcda saat program berjalan.
  • -ftest-coverage: Menyimpan informasi struktur control-flow graph ke file .gcno saat compile.

Kedua flag harus disertakan saat compile dan link. Saat program dijalankan, file .gcda ditulis ke direktori kerja. Alat gcov kemudian membaca .gcno dan .gcda untuk menghasilkan laporan human-readable berformat .gcov.

Modul yang Diuji: grade_letter #

Fungsi grade_letter(int score) memetakan nilai numerik ke huruf grade 'A' hingga 'E'. Terdapat enam cabang: lima grade valid dan satu kasus invalid (?). Berikut implementasinya:

c
/* File: grade.h */
#ifndef GRADE_H
#define GRADE_H
char grade_letter(int score);
#endif
c
/* File: grade.c — modul yang akan diukur coverage-nya */
#include "grade.h"

char grade_letter(int score) {
    if (score < 0 || score > 100) return '?';
    if (score >= 90) return 'A';
    if (score >= 80) return 'B';
    if (score >= 70) return 'C';
    if (score >= 60) return 'D';
    return 'E';
}

Test Pertama: Tidak Mencapai Full Coverage #

Test berikut hanya menguji lima grade valid—cabang invalid ('?') tidak diuji. Kita akan menggunakan gcov untuk menemukan celah ini.

c
/* File: test_grade.c — test yang tidak full coverage */
#include <assert.h>
#include <stdio.h>
#include "grade.h"

int main(void) {
    assert(grade_letter(95) == 'A');
    assert(grade_letter(85) == 'B');
    assert(grade_letter(75) == 'C');
    assert(grade_letter(65) == 'D');
    assert(grade_letter(50) == 'E');
    printf("Semua test lulus.\n");
    return 0;
}

Build dengan coverage dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -fprofile-arcs -ftest-coverage \
    -o test_grade test_grade.c grade.c && ./test_grade

Output yang Dihasilkan:

text
Semua test lulus.

Setelah program berjalan, GCC menulis file test_grade-grade.gcno (saat compile) dan test_grade-grade.gcda (saat runtime). Nama file diawali dengan nama executable agar tidak bentrok bila beberapa test runner dijalankan. Jalankan gcov dengan nama object file lengkap:

bash
gcov test_grade-grade.gcno

Output yang Dihasilkan:

text
File 'grade.c'
Lines executed:100.00% of 7
Creating 'grade.c.gcov'

Meskipun gcov melaporkan 100% line coverage, file .gcov mengungkap rincian cabang:

text
        -:    0:Source:grade.c
        -:    0:Graph:test_grade-grade.gcno
        -:    0:Data:test_grade-grade.gcda
        -:    0:Runs:1
        -:    1:/* File: grade.c — modul yang akan diukur coverage-nya */
        -:    2:#include "grade.h"
        -:    3:
        5:    4:char grade_letter(int score) {
       5*:    5:    if (score < 0 || score > 100) return '?';
        5:    6:    if (score >= 90) return 'A';
        4:    7:    if (score >= 80) return 'B';
        3:    8:    if (score >= 70) return 'C';
        2:    9:    if (score >= 60) return 'D';
        1:   10:    return 'E';
        -:   11:}

Mari kita telaah baris-baris kunci di atas:

  1. Baris 5: (kolom pertama 5): Nomor di kolom pertama adalah jumlah kali baris tersebut dieksekusi. Baris 4 dieksekusi 5 kali—sekali untuk setiap pemanggilan grade_letter di test.
  2. Baris 5 (5*: 5: if (score < 0 || score > 100) return '?';): Angka 5 di kolom pertama menunjukkan kondisi dievaluasi 5 kali. Tanda bintang * setelah angka adalah indikator branch coverage—mengindikasikan bahwa tidak semua cabang dari kondisi ini dieksekusi. Dalam kasus kita, cabang score < 0 || score > 100 selalu false (tidak pernah true), sehingga return '?' tidak pernah dieksekusi.
  3. Baris 6 (5: 6: if (score >= 90) return 'A';): Dieksekusi 5 kali. Salah satunya mengembalikan 'A' (saat score = 95), empat sisanya melanjutkan ke baris berikutnya.
  4. Baris 7 (4: 7: if (score >= 80) return 'B';): Dieksekusi 4 kali—lima pemanggilan dikurangi satu yang sudah kembali dengan 'A'. Satu pemanggilan (score = 85) mengembalikan 'B', tiga sisanya melanjutkan.
  5. Baris 10 (1: 10: return 'E';): Dieksekusi sekali untuk score = 50. Pola ini menunjukkan bahwa setiap cabang (kecuali '?') diuji tepat satu kali.
  6. Tanda -: di awal baris komentar/#include: gcov menandai baris non-executable dengan -—tidak ada eksekusi yang tercatat.

Untuk mencapai branch coverage penuh, kita tambahkan dua test untuk kasus invalid:

c
/* File: test_grade_full.c — test yang menambahkan kasus invalid */
#include <assert.h>
#include <stdio.h>
#include "grade.h"

int main(void) {
    assert(grade_letter(95) == 'A');
    assert(grade_letter(85) == 'B');
    assert(grade_letter(75) == 'C');
    assert(grade_letter(65) == 'D');
    assert(grade_letter(50) == 'E');
    /* Cabang invalid */
    assert(grade_letter(-1) == '?');
    assert(grade_letter(101) == '?');
    printf("Semua test lulus (full branch coverage).\n");
    return 0;
}

Build dan jalankan kembali gcov:

bash
rm -f *.gcda *.gcno *.gcov
gcc -std=c11 -Wall -Wextra -fprofile-arcs -ftest-coverage \
    -o test_grade_full test_grade_full.c grade.c && ./test_grade_full
gcov test_grade_full-grade.gcno

Output yang Dihasilkan:

text
Semua test lulus (full branch coverage).
File 'grade.c'
Lines executed:100.00% of 7
Creating 'grade.c.gcov'

File grade.c.gcov setelah test diperluas:

text
        -:    0:Source:grade.c
        -:    0:Graph:test_grade_full-grade.gcno
        -:    0:Data:test_grade_full-grade.gcda
        -:    0:Runs:1
        -:    1:/* File: grade.c — modul yang akan diukur coverage-nya */
        -:    2:#include "grade.h"
        -:    3:
        7:    4:char grade_letter(int score) {
        7:    5:    if (score < 0 || score > 100) return '?';
        5:    6:    if (score >= 90) return 'A';
        4:    7:    if (score >= 80) return 'B';
        3:    8:    if (score >= 70) return 'C';
        2:    9:    if (score >= 60) return 'D';
        1:   10:    return 'E';
        -:   11:}

Tanda bintang * pada baris 5 hilang karena kedua cabang (true dan false) sudah dieksekusi. Angka eksekusi baris 4 dan 5 naik dari 5 menjadi 7—dua pemanggilan tambahan untuk score = -1 dan score = 101, keduanya memicu return '?'.

Tipe Coverage yang Didukung gcov #

gcov melaporkan empat tipe coverage. Kita dapat memilih tipe yang difokuskan:

  • Line coverage: Persentase baris kode yang dieksekusi. Paling dasar, paling mudah dicapai.
  • Branch coverage: Persentase cabang kondisional yang dieksekusi. Lebih ketat daripada line coverage—satu baris if menghasilkan dua cabang.
  • Function coverage: Persentase fungsi yang dipanggil. Berguna untuk modul dengan banyak fungsi kecil.
  • Condition coverage: Persentase kondisi individual pada ekspresi majemuk (misalnya a || b memiliki dua kondisi).
bash
# Lapor dengan branch coverage
gcov -b test_grade_full-grade.gcno

Workflow Coverage di Proyek Besar #

Untuk proyek dengan banyak file, workflow coverage mengikuti pola berikut:

  1. Build seluruh proyek dengan flag coverage ke direktori terpisah (misalnya build-cov/) agar tidak mengganggu binary produksi.
  2. Jalankan seluruh test suite—file .gcda dihasilkan untuk setiap object file.
  3. Jalankan gcov untuk setiap file atau gunakan wrapper untuk mengumpulkan hasil.
  4. Hasilkan laporan HTML dengan lcov/genhtml bila tersedia—jauh lebih mudah dibaca daripada file .gcov per file.
  5. Tetapkan target coverage (misalnya 90% line, 80% branch) sebagai gerbang CI.