C也可以 TDD

參考說明載點

原文連結

C 其實也可以測試驅動開發的。

以下透過這個保齡球計分程式體驗 TDD in C 是怎樣的感覺(無需搭配其他框架)。

建立 bowling_game_test.c , 撰寫第一個測試 test_gutter_game()

bowling_game_test.c
#include <assert.h>
#include <stdbool.h>

static void test_gutter_game()
{
    assert(false && 'First test');
}

int main()
{
    test_gutter_game();
  
    return 0;
}

執行後測試失敗。

修改 test_gutter_game() 讓測試通過

bowling_game_test.c
#include <assert.h>
#include <stdbool.h>

static void test_gutter_game()
{
    bowling_game_init();

    for (int i = 0; i < 20; i ++) {
        bowling_game_roll(0);
    }
    
    //總分應該為0分
    assert(0 == bowling_game_score() && "test_gutter_game()");
}

int main()
{
    test_gutter_game();
}

執行測試
=> 錯誤訊息: bowling_game_init(), bowling_game_roll(), bowling_game_score() 都尚未定義,
=> 測試失敗

撰寫 bowling_game.h 和 bowling_game.c,實作上面三個函式

原本範例是把 bowling_game.h 和 bowling_game.c 拆成兩動,這邊我是覺得應該以"足以解決一個測試" 的動作當一動比較適當,因此只做將兩個步驟合成一個。

bowling_game.h
void bowling_game_init();
void bowling_game_roll(int pins);
int bowling_game_score();

bowling_game.c
#include "bowling_game.h"

static int score;

void bowling_game_init() {
}

void bowling_game_roll(int pins) {
}

int bowling_game_score() {
    return -1;
}

執行測試 => 錯誤訊息

assert(0 == bowling_game_score() && "test_gutter_game()");

=> 測試失敗

改寫程式碼讓 (0 == bowling_game_score() && "test_gutter_game()")

bowling_game.c
#include "bowling_game.h"

static int score;

void bowling_game_init() {
    score = 0;
}

void bowling_game_roll(int pins) {
}

int bowling_game_score() {
    return score;
}

執行測試 => 沒有發生錯誤 => 測試通過。

撰寫新的測試 test_all_ones()

scenario:

每一次丟球都擊倒一個球瓶。

bowling_game_test.c
// ....

static void test_all_ones() {
    bowling_game_init();

    for (int i = 0; i < 20; i ++) {
        bowling_game_roll(1);
    }

    //總分應該為20分
    assert(20 == bowling_game_score() && "test_all_ones()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    
    return 0;
}

執行測試
=> 錯誤訊息

Assertion failed: (20 == bowling_game_score() && "test_all_ones()")

=> 測試失敗

修改 bowling_game_roll() 讓測試通過

bowling_game_test.c
// ...

void bowling_game_roll(int pins) {
    score += pins;
}

// ...

執行測試 => 沒有出現錯誤 => 測試通過

重複的程式碼包裝成函式

test_gutter_game() 和 test_all_ones() 執行的 for 迴圈可包裝成函式共用,程式碼重構後如下:

bowling_game_test.c
// ...

static void roll_many(int n, int pins) {
    for (int i = 0; i < n; i ++) {
        bowling_game_roll(pins);
    }
}

static void test_gutter_game() {
    bowling_game_init();

    roll_many(20, 0);

    //總分應該為0分
    assert(0 == bowling_game_score() && "test_gutter_game()");
}

static void test_all_ones() {
    bowling_game_init();

    roll_many(20, 1);

    // 總分應該為20分
    assert(20 == bowling_game_score() && "test_all_ones()");
}

// ...

執行測試 => 沒有錯誤訊息 => 測試通過

撰寫新的測試 test_one_spare()

bowling_game_test.c
// ...

static void test_one_spare() {
    bowling_game_init();

    // 先投三次
    bowling_game_roll(5);
    bowling_game_roll(5); // 得到一個 spare
    bowling_game_roll(3);

    // 餘下的 17 次假設擊倒球瓶全部為 0 
    roll_many(17, 0);

    assert(16 == bowling_game_score() && "test_one_spare()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();

    return 0;
}

執行測試
=> 錯誤訊息

Assertion failed: (16 == bowling_game_score() && "test_one_spare()")

=> 測試失敗

檢視一下程式碼,可以發現兩個問題:

1. bowling_game_roll() 有計算分數,但函式名稱看不出來有他這個功用
2. bowling_game_score() 沒有計算的動作,但函式名稱會讓人誤以為有計算的動作

=> 這樣的設計是有問題的,各函式該負責的事情搞錯了。

重構 bowling_game.c

bowling_game.c
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {MAX_ROLLS = 21};
static int rolls[MAX_ROLLS];
static int current_roll;

void bowling_game_init() {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        rolls[i] = 0;
    }

    current_roll = 0;
}

void bowling_game_roll(int pins) {
    rolls[current_roll] = pins;

    current_roll ++;
}

int bowling_game_score() {
    int score = 0;

    for (int i = 0; i < MAX_ROLLS; i ++) {
        score += rolls[i];
    }

    return score;
}

執行測試
=> 錯誤訊息

Assertion failed: (16 == bowling_game_score() && "test_one_spare()")

=> 測試未通過

bowling_game_score() 的 spare 功能並未實作出來,繼續改寫。

改寫 bowling_game_score() 以通過 test_one_spare()

bowling_game_test.c
// ...

// static void test_one_spare() {
//     bowling_game_init();

//     // 先投三次
//     bowling_game_roll(5);
//     bowling_game_roll(5); // 得到一個 spare
//     bowling_game_roll(3);

//     // 餘下的 17 次假設擊倒球瓶全部為 0 
//     roll_many(17, 0);

//     assert(16 == bowling_game_score() && "test_one_spare()");
// }

int main() {
    test_gutter_game();
    test_all_ones();
    //test_one_spare();

    return 0;
}

註解掉test_one_spare() 是因為我們要重新設計 bowling_game_score(),透過原本已經 passed 的 test_gutter_game() 和 test_all_ones() 檢驗改寫的 bowling_game_score() 是否正確。

bowling_game.c
//...

int bowling_game_score() {
    int score = 0;
    int i = 0;

    for (int frame = 0; frame < 10; ++ frame) {
        score += rolls[i] + rolls[i + 1];
        i += 2;
    }

    return score;
}

//...

執行測試 => 沒有錯誤訊息 => 測試通過

表示 bowling_game_score() 重構後是正確的,因此我們可以繼續剛剛註解掉的 test_one_spare()。

取消對 test_one_spare() 的註解

bowling_game_test.c
// ...

static void test_one_spare() {
    bowling_game_init();

    // 先投三次
    bowling_game_roll(5);
    bowling_game_roll(5); // 得到一個 spare
    bowling_game_roll(3);

    // 餘下的 17 次假設擊倒球瓶全部為 0 
    roll_many(17, 0);

    assert(16 == bowling_game_score() && "test_one_spare()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();

    return 0;
}

執行測試
=> 出現錯誤訊息

Assertion failed: (16 == bowling_game_score() && "test_one_spare()")

繼續針對 bowling_game_score() 改寫 以通過 test_one_spare()

bowling_game.c
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {
    MAX_ROLLS = 21,
    FRAME_LENGTH = 10,
    MAX_SCORE_PER_FRAME = 10
};
static int rolls[MAX_ROLLS];
static int current_roll;

void bowling_game_init() {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        rolls[i] = 0;
    }

    current_roll = 0;
}

void bowling_game_roll(int pins) {
    rolls[current_roll] = pins;

    current_roll ++;
}

int bowling_game_score() {
    int score = 0;
    int i = 0;

    for (int frame = 0; frame < FRAME_LENGTH; ++ frame) {
        if (MAX_SCORE_PER_FRAME == rolls[i] + rolls[i + 1]) {
            score += MAX_SCORE_PER_FRAME + rolls[i + 2];
            i += 2;
        } else {
            score += rolls[i] + rolls[i + 1];
            i += 2;
        }
    }

    return score;
}

執行測試 => 沒有錯誤訊息 => 測試通過

修改bowling_game.c 增加程式碼可讀性

bowling_game.c
#include <stdbool.h>
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {
    MAX_ROLLS = 21,
    FRAME_LENGTH = 10,
    MAX_SCORE_PER_FRAME = 10
};
static int rolls[MAX_ROLLS];
static int current_roll;

static bool is_square(int roll_index) {
    return (10 == rolls[roll_index] + rolls[roll_index + 1]);
}

void bowling_game_init() {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        rolls[i] = 0;
    }

    current_roll = 0;
}

void bowling_game_roll(int pins) {
    rolls[current_roll] = pins;

    current_roll ++;
}

int bowling_game_score() {
    int score = 0;
    int roll_index = 0;

    for (int frame = 0; frame < FRAME_LENGTH; ++ frame) {
        if (is_square(roll_index)) {
            score += MAX_SCORE_PER_FRAME + rolls[roll_index + 2];
            roll_index += 2;
        } else {
            score += rolls[roll_index] + rolls[roll_index + 1];
            roll_index += 2;
        }
    }

    return score;
}

執行測試 => 沒有錯誤訊息 => 測試通過

撰寫新的測試 test_one_strike()

bowling_game_test.c
// ...

static void test_one_strike() {
    bowling_game_init();

    bowling_game_roll(10);
    bowling_game_roll(3);
    bowling_game_roll(4);

    roll_many(17, 0);

    assert(24 == bowling_game_score() && "test_one_strike");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();
    test_one_strike();

    return 0;
}

執行測試
=> 發生錯誤訊息

Assertion failed: (24 == bowling_game_score() && "test_one_strike")

=> 測試失敗

修改 bowling_game.c 以通過測試 test_one_strike()

bowling_game.c
// ...

static bool is_strike(int roll_index) {
    return 10 == rolls[roll_index];
}

// ...

int bowling_game_score() {
    int score = 0;
    int roll_index = 0;

    for (int frame = 0; frame < FRAME_LENGTH; ++ frame) {
        if (is_strike(roll_index)) {
            score += 10 + rolls[roll_index + 1] + rolls[roll_index + 2];

            roll_index += 1; 
        } else if (is_square(roll_index)) {
            score += MAX_SCORE_PER_FRAME + rolls[roll_index + 2];

            roll_index += 2;
        } else {
            score += rolls[roll_index] + rolls[roll_index + 1];

            roll_index += 2;
        }
    }

    return score;
}

執行測試 => 沒有錯誤訊息 => 測試通過

修改 bowling_game.c 增加程式碼可讀性

bowling_game.c
#include <stdbool.h>
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {
    MAX_ROLLS = 21,
    FRAME_LENGTH = 10,
    MAX_SCORE_PER_FRAME = 10
};
static int rolls[MAX_ROLLS];
static int current_roll;

static bool is_square(int roll_index) {
    return (MAX_SCORE_PER_FRAME == rolls[roll_index] + rolls[roll_index + 1]);
}

static bool is_strike(int roll_index) {
    return MAX_SCORE_PER_FRAME == rolls[roll_index];
}

static int strike_score(int roll_index) {
    return MAX_SCORE_PER_FRAME + rolls[roll_index + 1] + rolls[roll_index + 2];
}

static int spare_score(int roll_index) {
    return MAX_SCORE_PER_FRAME + rolls[roll_index + 2];
}

static int normal_score(int roll_index) {
    return rolls[roll_index] + rolls[roll_index + 1];
}

void bowling_game_init() {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        rolls[i] = 0;
    }

    current_roll = 0;
}

void bowling_game_roll(int pins) {
    rolls[current_roll] = pins;

    current_roll ++;
}

int bowling_game_score() {
    int score = 0;
    int roll_index = 0;

    for (int frame = 0; frame < FRAME_LENGTH; ++ frame) {
        if (is_strike(roll_index)) {
            score += strike_score(roll_index);

            roll_index += 1; 
        } else if (is_square(roll_index)) {
            score += spare_score(roll_index);

            roll_index += 2;
        } else {
            score += normal_score(roll_index);

            roll_index += 2;
        }
    }

    return score;
}

撰寫新的測試 test_perfect_game()

bowling_game_test.c
// ...

static void test_perfect_game() {
    bowling_game_init();

    roll_many(12, 10);

    assert(300 == bowling_game_score() && "test_perfect_game()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();
    test_one_strike();
    test_perfect_game();

    return 0;
}


執行測試 => 沒有錯誤訊息 => 測試通過

小結

到這邊程式的撰寫就算完成了,但用了 rolls 和 current_roll 兩個全域變數不是很好,可以再進一步將這兩個變數包進 struct

再次重構

bowling_game_test.c
#include "bowling_game.h"

#include <assert.h>
#include <stdbool.h>

static void roll_many(struct bowling_game * game, int n, int pins) {
    for (int i = 0; i < n; i ++) {
        bowling_game_roll(game, pins);
    }
}

static void test_gutter_game() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    roll_many(game, 20, 0);

    //總分應該為0分
    assert(0 == bowling_game_score(game) && "test_gutter_game()");
}

static void test_all_ones() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    roll_many(game, 20, 1);

    // 總分應該為20分
    assert(20 == bowling_game_score(game) && "test_all_ones()");
}

static void test_one_spare() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    // 先投三次
    bowling_game_roll(game, 5);
    bowling_game_roll(game, 5); // 得到一個 spare
    bowling_game_roll(game, 3);

    // 餘下的 17 次假設擊倒球瓶全部為 0 
    roll_many(game, 17, 0);

    assert(16 == bowling_game_score(game) && "test_one_spare()");
}

static void test_one_strike() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    bowling_game_roll(game, 10);
    bowling_game_roll(game, 3);
    bowling_game_roll(game, 4);

    roll_many(game, 17, 0);

    assert(24 == bowling_game_score(game) && "test_one_strike()");
}

static void test_perfect_game() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    roll_many(game, 12, 10);

    assert(300 == bowling_game_score(game) && "test_perfect_game()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();
    test_one_strike();
    test_perfect_game();

    return 0;
}


bowling_game.h
struct bowling_game;
struct bowling_game * bowling_game_create();

void bowling_game_init(struct bowling_game * game);
void bowling_game_roll(struct bowling_game * game, int pins);
int bowling_game_score(struct bowling_game * game);

bowling_game.c
#include <stdbool.h>
#include <stdlib.h>
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {
    MAX_ROLLS = 21,
    FRAME_LENGTH = 10,
    MAX_SCORE_PER_FRAME = 10
};

struct bowling_game {
    int rolls[MAX_ROLLS];
    int current_roll;
};

struct bowling_game * bowling_game_create() {
    struct bowling_game * game = malloc(sizeof(struct bowling_game));

    for (int i = 0; i < MAX_ROLLS; i ++) {
        game->rolls[i] = 0;
    }

    game->current_roll = 0;

    return game;
}

static bool is_square(struct bowling_game * game, int roll_index) {
    return (MAX_SCORE_PER_FRAME == game->rolls[roll_index] + game->rolls[roll_index + 1]);
}

static bool is_strike(struct bowling_game * game, int roll_index) {
    return MAX_SCORE_PER_FRAME == game->rolls[roll_index];
}

static int strike_score(struct bowling_game * game, int roll_index) {
    return MAX_SCORE_PER_FRAME + game->rolls[roll_index + 1] + game->rolls[roll_index + 2];
}

static int spare_score(struct bowling_game * game, int roll_index) {
    return MAX_SCORE_PER_FRAME + game->rolls[roll_index + 2];
}

static int normal_score(struct bowling_game * game, int roll_index) {
    return game->rolls[roll_index] + game->rolls[roll_index + 1];
}

void bowling_game_destroy(struct bowling_game * game) {
    free(game);

    game = NULL;
}

void bowling_game_init(struct bowling_game * game) {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        game->rolls[i] = 0;
    }

    game->current_roll = 0;
}

void bowling_game_roll(struct bowling_game * game, int pins) {
    game->rolls[game->current_roll] = pins;

    game->current_roll ++;
}

int bowling_game_score(struct bowling_game * game) {
    int score = 0;
    int roll_index = 0;

    for (int frame = 0; frame < FRAME_LENGTH; frame ++) {
        if (is_strike(game, roll_index)) {
            score += strike_score(game, roll_index);

            roll_index += 1; 
        } else if (is_square(game, roll_index)) {
            score += spare_score(game, roll_index);

            roll_index += 2;
        } else {
            score += normal_score(game, roll_index);

            roll_index += 2;
        }
    }

    return score;
}

感謝您的耐心閱讀。

程式碼打包下載

Comments

comments powered by Disqus