Setelah program utuh berjalan, tugas terakhir adalah memastikan perhitungan tetap benar saat kode berubah di masa depan. Pada bagian ini kita menulis tests.c — sebuah test runner mandiri tanpa framework pihak ketiga. Runner ini memanggil kalkulator_hitung dengan input dan nilai yang diharapkan, lalu mencatat setiap test sebagai LULUS atau GAGAL. Kita juga menyusun Makefile final dengan target test agar satu perintah make test langsung membangun dan menjalankan test runner.
Konsep #
Filosofi pengujian yang kita anut: logika murni dipisahkan dari I/O sehingga dapat diuji tanpa menyimulasikan keyboard. Modul calculator.c hanya bergantung pada input parameter dan pointer keluaran — tidak memanggil printf atau fgets. Akibatnya, tests.c dapat menguji setiap operasi dengan satu baris kode per test.
Karena perhitungan melibatkan double, kita tidak membandingkan kesetaraan persis (==) melainkan menggunakan toleransi epsilon. Fungsi pembantu hampir_sama(a, b) memulangkan true bila |a - b| < 1e-9. Ini menghindari masalah presisi floating-point yang membuat 0.1 + 0.2 != 0.3 dalam banyak bahasa.
Setiap test ditandai dengan macro PERIKSA(kondisi, nama) yang mencatat lulus/gagal dan menambah penghitung. Pada akhir main, runner mencetak ringkasan dan memulangkan kode exit 0 bila semua lulus, atau 1 bila ada yang gagal. Konvensi ini penting agar test runner dapat diintegrasikan ke CI/CD.
Implementasi #
Test pertama memeriksa penambahan:
static void test_tambah(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_TAMBAH, 5.0, 3.0, &hasil);
PERIKSA(s == CALC_OK && hampir_sama(hasil, 8.0), "Tambah 5 + 3 = 8");
}Test pembagian dengan nol memastikan status error yang tepat:
static void test_bagi_nol(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_BAGI, 5.0, 0.0, &hasil);
PERIKSA(s == CALC_ERR_BAGI_NOL, "Bagi dengan nol menolak");
}Macro PERIKSA merangkum pencatatan hasil:
#define PERIKSA(kondisi, nama) \
do { \
if (kondisi) { \
test_lulus++; \
printf("[LULUS] %s\n", nama); \
} else { \
test_gagal++; \
printf("[GAGAL] %s\n", nama); \
} \
} while (0)Kode Lengkap #
Berkas tests.c lengkap:
#include "calculator.h"
#include <stdio.h>
#include <math.h>
#include <string.h>
static int test_lulus = 0;
static int test_gagal = 0;
static int hampir_sama(double a, double b) {
return fabs(a - b) < 1e-9;
}
#define PERIKSA(kondisi, nama) \
do { \
if (kondisi) { \
test_lulus++; \
printf("[LULUS] %s\n", nama); \
} else { \
test_gagal++; \
printf("[GAGAL] %s\n", nama); \
} \
} while (0)
static void test_tambah(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_TAMBAH, 5.0, 3.0, &hasil);
PERIKSA(s == CALC_OK && hampir_sama(hasil, 8.0), "Tambah 5 + 3 = 8");
}
static void test_kurang(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_KURANG, 10.0, 4.0, &hasil);
PERIKSA(s == CALC_OK && hampir_sama(hasil, 6.0), "Kurang 10 - 4 = 6");
}
static void test_kali(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_KALI, 2.5, 4.0, &hasil);
PERIKSA(s == CALC_OK && hampir_sama(hasil, 10.0), "Kali 2.5 * 4 = 10");
}
static void test_bagi(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_BAGI, 10.0, 4.0, &hasil);
PERIKSA(s == CALC_OK && hampir_sama(hasil, 2.5), "Bagi 10 / 4 = 2.5");
}
static void test_bagi_nol(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_BAGI, 5.0, 0.0, &hasil);
PERIKSA(s == CALC_ERR_BAGI_NOL, "Bagi dengan nol menolak");
}
static void test_label_dan_simbol(void) {
PERIKSA(strcmp(kalkulator_label(OP_TAMBAH), "Tambah") == 0,
"Label OP_TAMBAH = \"Tambah\"");
PERIKSA(strcmp(kalkulator_simbol(OP_BAGI), "/") == 0,
"Simbol OP_BAGI = \"/\"");
}
int main(void) {
test_tambah();
test_kurang();
test_kali();
test_bagi();
test_bagi_nol();
test_label_dan_simbol();
printf("\nRingkasan: %d lulus, %d gagal\n", test_lulus, test_gagal);
return test_gagal == 0 ? 0 : 1;
}Berkas Makefile final dengan target test:
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -g
TARGET = program
TEST_TARGET = test_runner
SRCS = main.c calculator.c
TEST_SRCS = tests.c calculator.c
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS) -lm
test: $(TEST_TARGET)
./$(TEST_TARGET)
$(TEST_TARGET): $(TEST_SRCS)
$(CC) $(CFLAGS) -o $(TEST_TARGET) $(TEST_SRCS) -lm
clean:
rm -f $(TARGET) $(TEST_TARGET)
.PHONY: test cleanPengujian #
Jalankan seluruh rangkaian test dengan satu perintah:
make testOutput yang Dihasilkan:
[LULUS] Tambah 5 + 3 = 8
[LULUS] Kurang 10 - 4 = 6
[LULUS] Kali 2.5 * 4 = 10
[LULUS] Bagi 10 / 4 = 2.5
[LULUS] Bagi dengan nol menolak
[LULUS] Label OP_TAMBAH = "Tambah"
[LULUS] Simbol OP_BAGI = "/"
Ringkasan: 7 lulus, 0 gagalVerifikasi bahwa exit code runner adalah 0:
./test_runner > /dev/null; echo $?Output yang Dihasilkan:
0Mari kita telaah komponen kunci pada tests.c dan Makefile:
- Baris
static int test_lulus = 0;: variabel global statik dibatasi pada translation unit ini sehingga tidak bertabrakan dengan simbol lain bila test runner di-link dengan modul tambahan. - Baris
static int hampir_sama(double a, double b): pembanding epsilon.1e-9cukup kecil untuk perhitungan biasa; untuk domain yang berbeda (keuangan, ilmiah) toleransi mungkin perlu disesuaikan. - Macro
PERIKSA: macro, bukan fungsi, karena__LINE__di macro melekat pada titik pemanggilan — berguna bila kita ingin melaporkan nomor baris kelak. Polado { ... } while (0)memastikan macro aman dipakai sebagai statement. - Baris
return test_gagal == 0 ? 0 : 1;: konvensi exit code Unix —0berarti sukses, non-zero berarti gagal. CI/CD memeriksa exit code untuk menentukan build pass/fail. - Makefile target
test: target ini bersifat phony (bukan berkas). Make mengeksekusi./$(TEST_TARGET)setelah build; bila runner memulangkan non-zero,makemelaporkan Error 2.
Proyek 01 selesai. Seluruh kode diuji dengan gcc -std=c11 -Wall -Wextra, tujuh test LULUS, dan program interaktif berperilaku sesuai harapan untuk input valid maupun invalid. Selanjutnya kita lanjut ke Proyek 02 yang membahas game tebak angka.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
calculator.c
Unduh berkas/* calculator.c -- implementasi logika perhitungan kalkulator terminal. */
#include "calculator.h"
/* Header ini diperlukan untuk strcmp di kalkulator_label. */
#include <string.h>
CalcStatus kalkulator_hitung(Operasi op, double a, double b, double *hasil) {
/* Pastikan pointer hasil tidak NULL sebelum menulis. */
if (hasil == 0) {
return CALC_ERR_OP_TIDAK_DIKETAHUI;
}
switch (op) {
case OP_TAMBAH:
*hasil = a + b;
return CALC_OK;
case OP_KURANG:
*hasil = a - b;
return CALC_OK;
case OP_KALI:
*hasil = a * b;
return CALC_OK;
case OP_BAGI:
if (b == 0.0) {
return CALC_ERR_BAGI_NOL;
}
*hasil = a / b;
return CALC_OK;
default:
return CALC_ERR_OP_TIDAK_DIKETAHUI;
}
}
const char *kalkulator_label(Operasi op) {
switch (op) {
case OP_TAMBAH: return "Tambah";
case OP_KURANG: return "Kurang";
case OP_KALI: return "Kali";
case OP_BAGI: return "Bagi";
default: return "Tidak Diketahui";
}
}
const char *kalkulator_simbol(Operasi op) {
switch (op) {
case OP_TAMBAH: return "+";
case OP_KURANG: return "-";
case OP_KALI: return "*";
case OP_BAGI: return "/";
default: return "?";
}
}
main.c
Unduh berkas/* main.c -- titik masuk kalkulator terminal.
*
* Memisahkan I/O (di main.c) dari logika perhitungan (di calculator.c)
* supaya calculator.c dapat diuji tanpa membaca input pengguna.
*/
#include "calculator.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Konstanta menu untuk pilihan keluar. */
#define MENU_KELUAR 5
/* Menampilkan daftar menu utama ke stdout. */
static void tampilkan_menu(void) {
printf("=== Kalkulator Terminal ===\n");
printf("1. Tambah\n");
printf("2. Kurang\n");
printf("3. Kali\n");
printf("4. Bagi\n");
printf("5. Keluar\n");
}
/* Membaca satu baris input dan memparse bilangan double.
*
* Mengembalikan 1 bila berhasil, 0 bila input tidak terurai sebagai angka.
*/
static int baca_angka(const char *label, double *out) {
char buffer[128];
printf("%s: ", label);
if (fgets(buffer, sizeof(buffer), stdin) == 0) {
return 0;
}
/* fgets menyimpan newline; kita andalkan strtod untuk berhenti di sana. */
char *sisa = 0;
double nilai = strtod(buffer, &sisa);
/* Bila seluruh isi buffer adalah whitespace atau kosong, gagal. */
if (sisa == buffer) {
return 0;
}
*out = nilai;
return 1;
}
/* Membaca pilihan menu 1-5 dari input. */
static int baca_pilihan(void) {
char buffer[128];
printf("Pilih operasi (1-5): ");
if (fgets(buffer, sizeof(buffer), stdin) == 0) {
return MENU_KELUAR;
}
/* Ambil karakter pertama yang bukan whitespace. */
char *p = buffer;
while (*p == ' ' || *p == '\t') {
p++;
}
if (*p < '1' || *p > '5') {
return 0;
}
return *p - '0';
}
/* Menanyakan apakah pengguna ingin menghitung lagi. */
static int tanya_lagi(void) {
char buffer[16];
printf("Hitung lagi? (y/t): ");
if (fgets(buffer, sizeof(buffer), stdin) == 0) {
return 0;
}
return buffer[0] == 'y' || buffer[0] == 'Y';
}
int main(void) {
int lanjut = 1;
while (lanjut) {
tampilkan_menu();
int pilihan = baca_pilihan();
if (pilihan == MENU_KELUAR) {
break;
}
if (pilihan < 1 || pilihan > 4) {
printf("Pilihan tidak dikenal.\n");
continue;
}
double a = 0.0;
double b = 0.0;
if (!baca_angka("Angka pertama", &a)) {
printf("Input angka pertama tidak valid.\n");
continue;
}
if (!baca_angka("Angka kedua", &b)) {
printf("Input angka kedua tidak valid.\n");
continue;
}
Operasi op = (Operasi)pilihan;
double hasil = 0.0;
CalcStatus status = kalkulator_hitung(op, a, b, &hasil);
if (status == CALC_OK) {
printf("Hasil: %g %s %g = %g\n",
a, kalkulator_simbol(op), b, hasil);
} else if (status == CALC_ERR_BAGI_NOL) {
printf("Error: tidak bisa membagi dengan nol.\n");
} else {
printf("Error: operasi tidak dikenal.\n");
}
lanjut = tanya_lagi();
}
printf("Terima kasih telah menggunakan Kalkulator Terminal.\n");
return 0;
}
tests.c
Unduh berkas/* tests.c -- rangkaian test sederhana untuk modul calculator.
*
* Tiap test memanggil kalkulator_hitung lalu membandingkan hasil dengan
* nilai yang diharapkan. Jika ada test yang gagal, program keluar dengan
* kode bukan nol.
*/
#include "calculator.h"
#include <stdio.h>
#include <math.h>
#include <string.h>
static int test_lulus = 0;
static int test_gagal = 0;
/* Pembanding aman untuk bilangan double. */
static int hampir_sama(double a, double b) {
return fabs(a - b) < 1e-9;
}
/* Macro untuk mencatat dan melaporkan hasil test. */
#define PERIKSA(kondisi, nama) \
do { \
if (kondisi) { \
test_lulus++; \
printf("[LULUS] %s\n", nama); \
} else { \
test_gagal++; \
printf("[GAGAL] %s\n", nama); \
} \
} while (0)
static void test_tambah(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_TAMBAH, 5.0, 3.0, &hasil);
PERIKSA(s == CALC_OK && hampir_sama(hasil, 8.0), "Tambah 5 + 3 = 8");
}
static void test_kurang(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_KURANG, 10.0, 4.0, &hasil);
PERIKSA(s == CALC_OK && hampir_sama(hasil, 6.0), "Kurang 10 - 4 = 6");
}
static void test_kali(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_KALI, 2.5, 4.0, &hasil);
PERIKSA(s == CALC_OK && hampir_sama(hasil, 10.0), "Kali 2.5 * 4 = 10");
}
static void test_bagi(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_BAGI, 10.0, 4.0, &hasil);
PERIKSA(s == CALC_OK && hampir_sama(hasil, 2.5), "Bagi 10 / 4 = 2.5");
}
static void test_bagi_nol(void) {
double hasil = 0.0;
CalcStatus s = kalkulator_hitung(OP_BAGI, 5.0, 0.0, &hasil);
PERIKSA(s == CALC_ERR_BAGI_NOL, "Bagi dengan nol menolak");
}
static void test_label_dan_simbol(void) {
PERIKSA(strcmp(kalkulator_label(OP_TAMBAH), "Tambah") == 0,
"Label OP_TAMBAH = \"Tambah\"");
PERIKSA(strcmp(kalkulator_simbol(OP_BAGI), "/") == 0,
"Simbol OP_BAGI = \"/\"");
}
int main(void) {
test_tambah();
test_kurang();
test_kali();
test_bagi();
test_bagi_nol();
test_label_dan_simbol();
printf("\nRingkasan: %d lulus, %d gagal\n", test_lulus, test_gagal);
return test_gagal == 0 ? 0 : 1;
}