iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
佛心分享-SideProject30

Mongory:打造跨語言、高效能的萬用查詢引擎系列 第 10

Day 9:測試先行(Unity)與小單元驅動的開發哲學

  • 分享至 

  • xImage
  •  

Day 8 之後,筆者決定以 C 語言重寫核心
第一步不是寫功能,而是把「可驗證」這件事建立起來
今天分享筆者在 Mongory-core 採用 Unity 測試框架的實戰做法,如何把系統拆成小單元,讓每一步都有落點

為什麼先寫測試

  • 介面先行:先決定模組的 API 與行為,再填入實作

  • 快速回饋:每次重構或最佳化(例如減少 callstack、調整遍歷)能立即驗證沒壞

  • 降低恐懼:C 沒有 GC 與例外可依賴,測試是唯一「安全網」

Unity 基本骨架(最小可複製)

#include "unity/unity.h"

void setUp(void) {}
void tearDown(void) {}

void test_truth(void) {
  TEST_ASSERT_TRUE(1);
}

int main(void) {
  UNITY_BEGIN();
  RUN_TEST(test_truth);
  return UNITY_END();
}

在 Mongory-core 專案中,Unity 已隨測試一起配置,讀者只要專注在每個模組的行為

安裝 Unity(專案內建腳本)

本專案已提供自動安裝腳本與 Makefile 目標:

# 進入 C core 專案根目錄
cd mongory-core

# 方式一:用 Makefile 下載/配置 Unity
make setup-unity

# 方式二:直接執行安裝腳本(等效於上面)
chmod +x scripts/setup_unity.sh
./scripts/setup_unity.sh

# 方法三:如果是在讀者自己的C專案

# create Unity directory
mkdir -p tests/unity

# download Unity files
curl -o tests/unity/unity.h https://raw.githubusercontent.com/ThrowTheSwitch/Unity/master/src/unity.h
curl -o tests/unity/unity.c https://raw.githubusercontent.com/ThrowTheSwitch/Unity/master/src/unity.c
curl -o tests/unity/unity_internals.h https://raw.githubusercontent.com/ThrowTheSwitch/Unity/master/src/unity_internals.h

Makefile 的 setup-unity 會檢查 Unity 是否已存在,若沒有就呼叫 scripts/setup_unity.sh 幫讀者把 Unity 下載到正確位置(之後 make test 便可直接使用)

在 Mongory-core 的實踐(小單元切分)

核心被拆成幾個基礎構件,每個構件都有專屬測試:

  • memory_pool:分配、倍增擴容、reset、trace、free 的生命週期行為
  • value:型別封裝、比較函式(comp)、字串化(to_str)
  • array:push/get/set/each/sort_by、越界行為
  • table:set/get/del/each、再雜湊(rehash)
  • matchers:eq/gt/gte/in/nin/field/and/or/elemMatch/every…

這些測試檔名(例如 mongory_array_test.cmongory_value_test.c)與模組一一對應,能快速定位問題

最小單元測試範例:memory_pool 與 array

#include "unity/unity.h"
#include "mongory-core/foundations/memory_pool.h"
#include "mongory-core/foundations/array.h"
#include "mongory-core/foundations/value.h"

void setUp(void) {}
void tearDown(void) {}

void test_pool_alloc_and_reset(void) {
  mongory_memory_pool *pool = mongory_memory_pool_new();
  void *p1 = pool->alloc(pool, 128);
  TEST_ASSERT_NOT_NULL(p1);
  pool->reset(pool); // 不應崩潰,之後仍可分配
  void *p2 = pool->alloc(pool, 64);
  TEST_ASSERT_NOT_NULL(p2);
  pool->free(pool);
}

void test_array_push_and_get(void) {
  mongory_memory_pool *pool = mongory_memory_pool_new();
  mongory_array *arr = mongory_array_new(pool);
  arr->push(arr, mongory_value_wrap_i(pool, 42));
  arr->push(arr, mongory_value_wrap_i(pool, 7));
  TEST_ASSERT_EQUAL_INT(2, (int)arr->count);
  TEST_ASSERT_EQUAL_INT(42, (int)arr->get(arr, 0)->data.i);
  TEST_ASSERT_EQUAL_INT(7,  (int)arr->get(arr, 1)->data.i);
  pool->free(pool);
}

int main(void) {
  UNITY_BEGIN();
  RUN_TEST(test_pool_alloc_and_reset);
  RUN_TEST(test_array_push_and_get);
  return UNITY_END();
}

重點是「行為」而不是內部實作:

  • pool reset 後仍可分配,pool free 後不能再 free
  • array 動態擴容對讀者透明,語意與 Ruby Array 類似

如何執行測試

mongory-core/ 目錄:

# 方式一:使用提供的 Makefile
make test # 最簡單但限平台 macos

# 方式二:使用提供的 script
./build.sh --test

# 方式三:使用 CMake(跨平台通用,應該)
mkdir -p build && cd build
cmake .. && make && ctest --output-on-failure | cat
# 如果沒 cmake 就要裝

若讀者是在 mongory-rb 的 submodule 之內,也可進入 ext/mongory_ext/mongory-core 以相同方式執行

設計哲學:小步快跑,能跑再快

  • 每個模組先定義 API,再寫 2–5 個關鍵情境測試,最後補邊界值
  • 優先測行為而非內部細節(例如 memory pool 的 chunk 倍增是細節,行為是「可分配、可 reset、可 free」)
  • 當要做效能優化(如 unwrap、遍歷順序、callstack 降低)時,先跑一輪測試確保語意未變

常見踩雷

  • 測試互相污染:記得建立/釋放 pool,避免跨測試共享狀態
  • 斷言錯誤位置難查:善用 Unity 的訊息與分組,單檔內多個 RUN_TEST
  • 未覆蓋錯誤流:Mongory-core 把錯誤掛在 pool 上(pool->error),測試時別忘了驗證

下一步

Day 10 會專講 memory_pool:為什麼選擇 chunk 倍增、如何在 O(log n) 控制擴容、reset 與 traced memory 的策略,並用測試帶著讀者一步步看清記憶體生命週期

專案(C Core)


上一篇
Day 8:為什麼 Ruby 不夠快?benchmark、目標與方法學
下一篇
Day 10:Memory pool 設計:chunk 倍增與 O(log n) 擴容
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言