はじめに
C言語のHello Worldを使い、テスト計画、テスト仕様、テスト設計、実装、実施を一通り体験します。サンプルは固定の Hello, World! を出力するだけです。
本文のコードを、指定したファイル名で保存して動かせます。 環境はWindows x64とVS Codeを想定し、Dockerは使いません。Ruby・GCC・Ceedlingはプロジェクト内に配置します。
| ツール | 役割 |
|---|---|
| Ceedling | テストの生成・ビルド・実行 |
| Unity | 値や文字列の比較 |
| CMock | 出力関数を置き換え、呼び出しを検証 |
UnityとCMockはCeedlingに同梱されています。ここでのUnityはC言語用テストフレームワークです。Ceedling公式
1 テスト計画
目的は、挨拶文と出力処理が要求どおりに動くことの確認です。要求は次の2つに絞ります。
- 挨拶文は
Hello, World!とする。 - 出力関数を1回呼び、その戻り値を呼び出し元へ返す。0は成功、-1は失敗とする。
単体テストでは文字列と出力関数とのやり取りを確認します。ターミナルへの実際の表示は、最後に実プログラムで確認します。完了条件は「3件のテスト成功」と「実際の挨拶文の表示」です。
2 テスト仕様とテスト設計
| ID | 条件 | 期待結果 | 確認方法 |
|---|---|---|---|
| TC-01 | 挨拶文を取得 | Hello, World! |
Unityで文字列比較 |
| TC-02 | 出力関数が0を返す | 正しい文字列で1回呼び、0を返す | CMockとUnity |
| TC-03 | 出力関数が-1を返す | 正しい文字列で1回呼び、-1を返す | CMockとUnity |
この表の条件と期待結果がテスト仕様、確認方法がテスト設計に当たります。hello.cをテスト対象とし、画面出力を担当するoutput.cだけをモックに置き換えます。
main → hello_run → output_write → puts
└─ hello_message
3 プロジェクト専用の環境を用意する
フォルダーを作る
C:\repos\hello-ceedlingなど、親フォルダーも含めて日本語のないパスに作成し、VS Codeで開きます。今回のGCCでは日本語を含む配置先でリンクに失敗したため、この条件にします。
hello-ceedling/
├─ .tools/ Ruby・GCC・gemの置き場
├─ .vscode/tasks.json
├─ app/main.c
├─ src/hello.h
├─ src/hello.c
├─ src/output.h
├─ src/output.c
├─ test/test_hello.c
├─ project.yml
└─ run.ps1
VS Codeの統合ターミナルで、プロジェクトのルートから次を実行すると必要なフォルダーを作れます。
New-Item -ItemType Directory -Force .tools, .vscode, app, src, test
RubyとGCCを展開する
次の2つをダウンロードし、既存の7-Zipなどで.toolsへ展開します。インストーラーは使いません。
-
RubyInstaller 3.4.11-1 x64の7zアーカイブ:展開されたフォルダーを
rubyへ改名する。 -
WinLibs GCC 14.2.0 UCRT r3のZIP:展開された
mingw64をそのまま使う。
最終的に.tools/ruby/bin/ruby.exeと.tools/mingw64/bin/gcc.exeが存在する配置にします。アーカイブを解凍しただけでフォルダーが二重になっていないか確認してください。
run.ps1
次をプロジェクトのルートに保存します。このスクリプトで専用環境のセットアップ、テスト、実プログラムの実行を行います。
param([ValidateSet('setup','versions','test','run')][string]$Action = 'test')
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
if ($PSScriptRoot -match '[^\x00-\x7F]') { throw 'Use an ASCII-only folder path.' }
if (!(Test-Path '.tools/ruby/bin/ruby.exe') -or !(Test-Path '.tools/mingw64/bin/gcc.exe')) {
throw 'Extract Ruby and GCC into .tools first.'
}
$env:PATH = "$PSScriptRoot\.tools\ruby\bin;$PSScriptRoot\.tools\mingw64\bin;$env:PATH"
$env:GEM_HOME = "$PSScriptRoot\.tools\gems"
$env:GEM_PATH = $env:GEM_HOME
$env:GEM_SPEC_CACHE = "$PSScriptRoot\.tools\cache"
foreach ($key in @('RUBYOPT','RUBYLIB','RUBYGEMS_GEMDEPS','RI_DEVKIT')) {
[Environment]::SetEnvironmentVariable($key, $null, 'Process')
}
$ceedling = "$env:GEM_HOME/gems/ceedling-1.1.10/bin/ceedling"
switch ($Action) {
'setup' {
& ruby .tools/ruby/bin/gem install ceedling:1.1.10 constructor:2.0.0 deep_merge:1.2.2 diy:1.1.2 parallel:1.28.0 rake:13.4.2 thor:1.5.0 unicode-display_width:3.3.0 unicode-emoji:4.3.0 --norc --ignore-dependencies --no-document --install-dir $env:GEM_HOME --clear-sources --source https://rubygems.org
}
'versions' {
& ruby --version
& gcc --version
& ruby $ceedling version
}
'test' { & ruby $ceedling test:all }
'run' {
New-Item -ItemType Directory build -Force | Out-Null
& gcc -std=c99 -Wall -Wextra -Werror -Isrc app/main.c src/hello.c src/output.c -o build/hello.exe
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& .\build\hello.exe
}
}
exit $LASTEXITCODE
環境変数は、後述のpowershell.exe -Fileで起動した子プロセス内だけで変更します。スクリプトをドットソースせず、この起動方法を使ってください。ユーザーやシステムのPATH、既存のgem置き場を変更する必要はありません。
gemのバージョンを固定し、ERBとbenchmarkには専用Rubyに同梱されたものを使っています。--ignore-dependenciesは、ここに列挙した組み合わせで新しいネイティブ拡張を取得しないための指定です。この構成ではMSYS2の追加導入を行いません。
依存ツールはフォルダー単位で分けられますが、OSの一時ファイルなどまで隔離する仕組みではありません。
初回セットアップ
統合ターミナルの作業場所がプロジェクトのルートであることを確認し、1つずつ実行して終了コード0を確認します。初回取得にはネットワーク接続が必要です。
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\run.ps1 setup
$LASTEXITCODE
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\run.ps1 versions
$LASTEXITCODE
Ruby・GCC・Ceedlingのバージョンが表示されれば、次へ進みます。-ExecutionPolicy Bypassは起動したプロセスだけへの指定です。PC全体の実行ポリシーを保存し直しません。
Extract Ruby and GCC into .tools first.と表示されたら、先ほどの2つの実行ファイルの配置を確認してください。gemの取得で失敗したら、そのエラーを解消してからsetupを再実行します。VS Codeのテスト・実行タスクが初回セットアップを自動で行うことはありません。
4 プログラムとテストを保存する
以下のコードをすべて保存してから、テストを実行します。生成モックを手書きする必要はありません。
src/hello.h
#ifndef HELLO_H
#define HELLO_H
const char *hello_message(void);
int hello_run(void);
#endif
src/hello.c
#include "hello.h"
#include "output.h"
const char *hello_message(void) { return "Hello, World!"; }
int hello_run(void) { return output_write(hello_message()); }
文字列を返す関数と、出力を呼び出す関数を分けています。生成した文字列には改行を含めず、実際の出力関数で改行を付けます。
src/output.h
#ifndef OUTPUT_H
#define OUTPUT_H
int output_write(const char *text);
#endif
src/output.c
#include "output.h"
#include <stdio.h>
int output_write(const char *text) { return puts(text) == EOF ? -1 : 0; }
app/main.c
#include "hello.h"
int main(void) { return hello_run() == 0 ? 0 : 1; }
実プログラムでは本物のoutput.cを使います。main.cをappへ分けることで、テスト用のmainと混ざらない構成にします。
test/test_hello.c
#include "unity.h"
#include "hello.h"
#include "mock_output.h"
void setUp(void) {}
void tearDown(void) {}
void test_TC01_message(void)
{
TEST_ASSERT_EQUAL_STRING("Hello, World!", hello_message());
}
void test_TC02_output_success(void)
{
output_write_ExpectAndReturn("Hello, World!", 0);
TEST_ASSERT_EQUAL_INT(0, hello_run());
}
void test_TC03_output_failure(void)
{
output_write_ExpectAndReturn("Hello, World!", -1);
TEST_ASSERT_EQUAL_INT(-1, hello_run());
}
TEST_ASSERT_EQUAL_STRINGで文字列、TEST_ASSERT_EQUAL_INTで戻り値を確認します。setUpとtearDownは各テストの前後に呼ばれ、今回は空にしています。
mock_output.hをincludeすると、Ceedlingがoutput.hからモックを生成します。output_write_ExpectAndReturnは、期待する文字列とモックの戻り値を設定します。1回分の期待なので、呼び出しが0回でも2回でも失敗します。CMock公式
project.yml
---
:project:
:build_root: build
:use_mocks: TRUE
:paths:
:test:
- test/**
:source:
- src/**
:include:
- src/**
:cmock:
:mock_prefix: mock_
:plugins:
:enabled:
- report_tests_pretty_stdout
5 テストと実プログラムを実行する
初回セットアップと、上記の全ファイルの保存が完了してから実行します。
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\run.ps1 test
$LASTEXITCODE
実測のテスト結果は次のとおりです。
TESTED: 3
PASSED: 3
FAILED: 0
IGNORED: 0
続いて、本物の出力処理をリンクした実プログラムをビルド・実行します。
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\run.ps1 run
$LASTEXITCODE
Hello, World!が表示され、終了コード0になれば成功です。掲載コードはWindows x64、Ruby 3.4.11、GCC 14.2.0、Ceedling 1.1.10、Unity 2.7.2、CMock 2.7.2で実行確認しました。
6 VS Codeのタスクから実行する
.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Hello: test",
"type": "process",
"command": "powershell.exe",
"args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "${workspaceFolder}/run.ps1", "test"],
"problemMatcher": []
},
{
"label": "Hello: run",
"type": "process",
"command": "powershell.exe",
"args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "${workspaceFolder}/run.ps1", "run"],
"problemMatcher": []
}
]
}
セットアップを済ませた後、コマンドパレットで「Tasks: Run Task」を選び、「Hello: test」または「Hello: run」を実行します。別プロセスのPowerShellで起動するため、専用環境のPATHは他のターミナルへ引き継がれません。VS Code Tasks公式
7 テストが不具合を検出するか確認する
src/hello.cの文字列だけをHello, World?へ変更し、テストを再実行してみます。テスト側の期待値は変更しません。
文字列が期待と異なるため、Unityの文字列比較とCMockの引数検証で失敗します。実際にこの変更を試したところ3件が失敗し、元に戻すと3件すべて成功しました。
最後に正しいコードへ戻し、testとrunを再実行します。後片付けでは、実行が終了してからこのプロジェクトの.toolsとbuildだけを削除できます。Gitを使う場合は、この2フォルダーを.gitignoreへ記載してください。
おわりに
小さなHello Worldでも、文字列の確認、外部関数の呼び出し、失敗時の戻り値をテストできます。計画で範囲を決め、仕様で期待結果を決め、設計で確認方法を決めてからコードへ落とし込む流れを、機能追加のときにも使ってみてください。