Pada bagian terakhir ini kita merapikan struktur kode menjadi modular dan menulis tests.c dengan sepuluh test case yang mencakup logika game_periksa, game_hitung_skor_aman, dan rentang game_angka_rahasia. Tujuan akhirnya adalah satu perintah make test yang membangun dan menjalankan seluruh rangkaian test tanpa intervensi manual.
Konsep #
Filosofi testing yang kita anut sama dengan Proyek 01: logika murni (di game.c) dipisahkan dari I/O (di main.c). Test runner tests.c meng-link modul game.c tetapi tidak pernah memanggil printf di main.c. Akibatnya, setiap test hanya perlu memanggil fungsi game dan membandingkan hasil dengan nilai yang diharapkan — tidak ada penyimulasian keyboard.
Tantangan khusus pada game ini adalah angka rahasia yang acak. Kita tidak dapat menguji game_angka_rahasia() terhadap nilai pasti, tetapi kita dapat menguji invariant: apakah hasil selalu berada di rentang [MIN, MAKs]? Test runner memanggil fungsi tersebut 1000 kali dan memeriksa setiap hasil — bila ada satu saja di luar rentang, test gagal.
Untuk fungsi deterministik seperti game_periksa dan game_hitung_skor_aman, kita dapat menulis test persis seperti pada Proyek 01. Setiap test memanggil fungsi dengan input tertentu dan membandingkan dengan output yang diharapkan.
Implementasi #
Macro PERIKSA mencatat lulus/gagal dan menambah penghitung:
static int lulus = 0;
static int gagal = 0;
#define PERIKSA(kondisi, nama) \
do { \
if (kondisi) { lulus++; printf("[LULUS] %s\n", nama); } \
else { gagal++; printf("[GAGAL] %s\n", nama); } \
} while (0)Test invariant rentang acak:
static void test_rentang_rahasia(void) {
game_init();
int semua_sah = 1;
for (int i = 0; i < 1000; i++) {
int r = game_angka_rahasia();
if (r < GAME_TEBAK_MIN || r > GAME_TEBAK_MAKS) {
semua_sah = 0;
break;
}
}
PERIKSA(semua_sah, "Angka rahasia selalu di rentang sah");
}Kode Lengkap #
Berkas tests.c lengkap:
#include "game.h"
#include <stdio.h>
static int lulus = 0;
static int gagal = 0;
#define PERIKSA(kondisi, nama) \
do { \
if (kondisi) { lulus++; printf("[LULUS] %s\n", nama); } \
else { gagal++; printf("[GAGAL] %s\n", nama); } \
} while (0)
static void test_rentang_rahasia(void) {
game_init();
int semua_sah = 1;
for (int i = 0; i < 1000; i++) {
int r = game_angka_rahasia();
if (r < GAME_TEBAK_MIN || r > GAME_TEBAK_MAKS) {
semua_sah = 0;
break;
}
}
PERIKSA(semua_sah, "Angka rahasia selalu di rentang sah");
}
static void test_periksa_benar(void) {
PERIKSA(game_periksa(50, 50, 1) == TEBAK_BENAR,
"Tebakan 50 dengan rahasia 50 -> BENAR");
}
static void test_periksa_kecil(void) {
PERIKSA(game_periksa(30, 50, 1) == TEBAK_KECIL,
"Tebakan 30 dengan rahasia 50 -> KECIL");
}
static void test_periksa_besar(void) {
PERIKSA(game_periksa(70, 50, 1) == TEBAK_BESAR,
"Tebakan 70 dengan rahasia 50 -> BESAR");
}
static void test_periksa_invalid(void) {
PERIKSA(game_periksa(0, 50, 1) == TEBAK_INVALID,
"Tebakan 0 di luar rentang -> INVALID");
}
static void test_periksa_habis(void) {
PERIKSA(game_periksa(50, 50, GAME_MAKS_RONDE + 1) == TEBAK_HABIS,
"Ronde > MAKs -> HABIS");
}
static void test_skor_ronde1(void) {
PERIKSA(game_hitung_skor_aman(1) == GAME_SKOR_AWAL,
"Menang ronde 1 -> skor penuh");
}
static void test_skor_ronde5(void) {
PERIKSA(game_hitung_skor_aman(5) == 60,
"Menang ronde 5 -> skor 60");
}
static void test_skor_invalid(void) {
PERIKSA(game_hitung_skor_aman(0) == 0, "Ronde 0 -> skor 0");
PERIKSA(game_hitung_skor_aman(GAME_MAKS_RONDE + 1) == 0,
"Ronde > MAKs -> skor 0");
}
int main(void) {
test_rentang_rahasia();
test_periksa_benar();
test_periksa_kecil();
test_periksa_besar();
test_periksa_invalid();
test_periksa_habis();
test_skor_ronde1();
test_skor_ronde5();
test_skor_invalid();
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}Berkas Makefile:
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -g
TARGET = program
TEST_TARGET = test_runner
SRCS = main.c game.c
TEST_SRCS = tests.c game.c
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS)
test: $(TEST_TARGET)
./$(TEST_TARGET)
$(TEST_TARGET): $(TEST_SRCS)
$(CC) $(CFLAGS) -o $(TEST_TARGET) $(TEST_SRCS)
clean:
rm -f $(TARGET) $(TEST_TARGET)
.PHONY: test cleanPengujian #
Jalankan seluruh rangkaian test:
make testOutput yang Dihasilkan:
[LULUS] Angka rahasia selalu di rentang sah
[LULUS] Tebakan 50 dengan rahasia 50 -> BENAR
[LULUS] Tebakan 30 dengan rahasia 50 -> KECIL
[LULUS] Tebakan 70 dengan rahasia 50 -> BESAR
[LULUS] Tebakan 0 di luar rentang -> INVALID
[LULUS] Ronde > MAKs -> HABIS
[LULUS] Menang ronde 1 -> skor penuh
[LULUS] Menang ronde 5 -> skor 60
[LULUS] Ronde 0 -> skor 0
[LULUS] Ronde > MAKs -> skor 0
Ringkasan: 10 lulus, 0 gagalVerifikasi exit code:
./test_runner > /dev/null; echo $?Output yang Dihasilkan:
0Mari kita telaah komponen kunci pada tests.c dan Makefile:
- Baris
game_init();di test pertama: walaupun test tidak membacagame_init()eksplisit di luartest_rentang_rahasia, pemanggilan di awal test tersebut memastikansrandsudah dipanggil sebelumgame_angka_rahasia()dipanggil 1000 kali. Tanpasrand, semua angka sama persis di setiap run — yang sebenarnya mempermudah debugging tetapi mengaburkan kewajaran distribusi. - Baris
for (int i = 0; i < 1000; i++): iterasi 1000 kali memberi probabilitas tinggi bahwa bila ada bug rentang, setidaknya satu iterasi akan menemukannya. Ini bukan jaminan mutlak — random number bisa saja kebetulan semua valid meski ada bug — tetapi praktis cukup untuk game. - Baris
break;di dalam loop: bila satu iterasi gagal, kita berhenti. Tidak perlu melanjutkan 999 iterasi sisanya karena kesimpulan test sudah jelas. - Baris
PERIKSA(game_periksa(50, 50, 1) == TEBAK_BENAR, ...): bandingkan enum dengan enum — tipe-safe, tidak ada cast. Compiler akan menolak bila kita menulisgame_periksa(...) == 0(compile tetapi mungkin kesalahan logika). - Baris
TEST_SRCS = tests.c game.c:main.csengaja tidak di-link ke test runner karenamain.cpunyamainsendiri. Tanpa ini, terjadi error “multiple definition ofmain”. - Target
$(TEST_TARGET): terpisah dari$(TARGET), sehinggamakemembangun keduanya secara terpisah.make testmembangun test runner lalu menjalankannya.
Proyek 02 selesai. Seluruh kode diuji dengan gcc -std=c11 -Wall -Wextra, sepuluh test LULUS, dan program interaktif berperilaku benar untuk input valid, invalid, maupun EOF. Selanjutnya kita lanjut ke Proyek 03 yang membahas manajemen kontak dengan persistensi ke file.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
game.c
Unduh berkas/* game.c -- implementasi logika game tebak angka. */
#include "game.h"
#include <stdlib.h>
#include <time.h>
void game_init(void) {
/* Seed generator dengan waktu saat ini. */
srand((unsigned int)time(NULL));
}
int game_angka_rahasia(void) {
/* rand() % N menghasilkan nilai [0, N-1]; kita geser ke rentang yang diinginkan. */
int rentang = GAME_TEBAK_MAKS - GAME_TEBAK_MIN + 1;
return GAME_TEBAK_MIN + (rand() % rentang);
}
TebakStatus game_periksa(int tebak, int rahasia, int ronde_kini) {
if (ronde_kini > GAME_MAKS_RONDE) {
return TEBAK_HABIS;
}
if (tebak < GAME_TEBAK_MIN || tebak > GAME_TEBAK_MAKS) {
return TEBAK_INVALID;
}
if (tebak == rahasia) {
return TEBAK_BENAR;
}
if (tebak < rahasia) {
return TEBAK_KECIL;
}
return TEBAK_BESAR;
}
int game_hitung_skor(int ronde_menang) {
/* Setiap ronde setelah ronde 1 mengurangi 10 poin. */
int pengurangan = (ronde_menang - 1) * 10;
int skor = GAME_SKOR_AWAL - pengurangan;
if (skor < 0) skor = 0;
return skor;
}
int game_hitung_skor_aman(int ronde_menang) {
/* Versi yang menolak input tidak valid. */
if (ronde_menang < 1 || ronde_menang > GAME_MAKS_RONDE) {
return 0;
}
return game_hitung_skor(ronde_menang);
}
const char *game_rentang_teks(void) {
static char buffer[32];
snprintf(buffer, sizeof(buffer), "%d sampai %d",
GAME_TEBAK_MIN, GAME_TEBAK_MAKS);
return buffer;
}
main.c
Unduh berkas/* main.c -- titik masuk game tebak angka.
*
* Memisahkan logika (game.c) dari I/O (main.c) supaya game.c dapat diuji
* tanpa menyimulasikan keyboard.
*/
#include "game.h"
#include <stdio.h>
#include <stdlib.h>
/* Membaca satu integer dari stdin.
* Mengembalikan 1 bila berhasil, 0 bila input tidak terurai, -1 bila EOF.
*/
static int baca_int(const char *prompt, int *out) {
char buffer[64];
printf("%s", prompt);
if (fgets(buffer, sizeof(buffer), stdin) == 0) return -1;
char *sisa = 0;
long nilai = strtol(buffer, &sisa, 10);
if (sisa == buffer) return 0;
*out = (int)nilai;
return 1;
}
static const char *status_pesan(TebakStatus s) {
switch (s) {
case TEBAK_BENAR: return "Tebakan benar!";
case TEBAK_KECIL: return "Tebakan terlalu kecil.";
case TEBAK_BESAR: return "Tebakan terlalu besar.";
case TEBAK_INVALID:return "Tebakan di luar rentang yang sah.";
case TEBAK_HABIS: return "Ronde sudah habis.";
default: return "Status tidak dikenal.";
}
}
int main(void) {
game_init();
int rahasia = game_angka_rahasia();
printf("=== Game Tebak Angka ===\n");
printf("Saya menyimpan angka antara %s.\n", game_rentang_teks());
printf("Anda punya %d ronde untuk menebak.\n\n", GAME_MAKS_RONDE);
int menang = 0;
int ronde_menang = 0;
for (int ronde = 1; ronde <= GAME_MAKS_RONDE; ronde++) {
int tebak = 0;
int hasil_baca = baca_int("Tebakan Anda: ", &tebak);
if (hasil_baca == -1) {
printf("\nInput berakhir (EOF). Berhenti.\n");
break;
}
if (hasil_baca == 0) {
printf("Input tidak valid. Coba lagi.\n");
ronde--; /* jangan pakai ronde untuk input invalid */
continue;
}
TebakStatus s = game_periksa(tebak, rahasia, ronde);
printf("Ronde %d: %s\n", ronde, status_pesan(s));
if (s == TEBAK_INVALID) {
ronde--; /* input invalid tidak mengurangi ronde */
continue;
}
if (s == TEBAK_BENAR) {
menang = 1;
ronde_menang = ronde;
break;
}
}
if (menang) {
int skor = game_hitung_skor_aman(ronde_menang);
printf("\nSelamat! Angka rahasia: %d\n", rahasia);
printf("Anda menang di ronde %d dengan skor %d.\n", ronde_menang, skor);
} else {
printf("\nMaaf, ronde habis. Angka rahasia: %d\n", rahasia);
}
return 0;
}
tests.c
Unduh berkas/* tests.c -- rangkaian test untuk modul game. */
#include "game.h"
#include <stdio.h>
static int lulus = 0;
static int gagal = 0;
#define PERIKSA(kondisi, nama) \
do { \
if (kondisi) { lulus++; printf("[LULUS] %s\n", nama); } \
else { gagal++; printf("[GAGAL] %s\n", nama); } \
} while (0)
static void test_rentang_rahasia(void) {
/* Karena angka acak, kita tes sebanyak 1000 kali untuk memastikan
* seluruh hasil berada dalam rentang sah.
*/
game_init();
int semua_sah = 1;
for (int i = 0; i < 1000; i++) {
int r = game_angka_rahasia();
if (r < GAME_TEBAK_MIN || r > GAME_TEBAK_MAKS) {
semua_sah = 0;
break;
}
}
PERIKSA(semua_sah, "Angka rahasia selalu di rentang sah");
}
static void test_periksa_benar(void) {
TebakStatus s = game_periksa(50, 50, 1);
PERIKSA(s == TEBAK_BENAR, "Tebakan 50 dengan rahasia 50 -> BENAR");
}
static void test_periksa_kecil(void) {
TebakStatus s = game_periksa(30, 50, 1);
PERIKSA(s == TEBAK_KECIL, "Tebakan 30 dengan rahasia 50 -> KECIL");
}
static void test_periksa_besar(void) {
TebakStatus s = game_periksa(70, 50, 1);
PERIKSA(s == TEBAK_BESAR, "Tebakan 70 dengan rahasia 50 -> BESAR");
}
static void test_periksa_invalid(void) {
TebakStatus s = game_periksa(0, 50, 1);
PERIKSA(s == TEBAK_INVALID, "Tebakan 0 di luar rentang -> INVALID");
}
static void test_periksa_habis(void) {
TebakStatus s = game_periksa(50, 50, GAME_MAKS_RONDE + 1);
PERIKSA(s == TEBAK_HABIS, "Ronde > MAKs -> HABIS");
}
static void test_skor_ronde1(void) {
int skor = game_hitung_skor_aman(1);
PERIKSA(skor == GAME_SKOR_AWAL, "Menang ronde 1 -> skor penuh");
}
static void test_skor_ronde5(void) {
int skor = game_hitung_skor_aman(5);
PERIKSA(skor == 60, "Menang ronde 5 -> skor 60");
}
static void test_skor_invalid(void) {
int skor = game_hitung_skor_aman(0);
PERIKSA(skor == 0, "Ronde 0 -> skor 0");
skor = game_hitung_skor_aman(GAME_MAKS_RONDE + 1);
PERIKSA(skor == 0, "Ronde > MAKs -> skor 0");
}
int main(void) {
test_rentang_rahasia();
test_periksa_benar();
test_periksa_kecil();
test_periksa_besar();
test_periksa_invalid();
test_periksa_habis();
test_skor_ronde1();
test_skor_ronde5();
test_skor_invalid();
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}