assertでテスト駆動C言語

競プロの問題を解くときにassert()でテストを書きながらRed-Green-Refactoringサイクルなテスト駆動開発をしたら捗った話。

assert()とは

プログラムの実行中にある条件が成立するかどうかチェックする機能で、その条件が満たされていないとエラーが発生してプログラムが終了します。

以下のようにして使います。

#include<stdio.h>
#include<assert.h>  // assert.hが必要

int main(void) {
  int value = 0;
  assert(value == 1); // 条件を満たさないのでここでエラーが発生する
  return 0;
}

上の例は極端な例ですが、ざっくりassert()の使い方の紹介です。

テスト駆動開発(TDD)とは

プログラムのロジックを書く前にまず、期待する動作のテストプログラムを書き、このテストが正常に失敗することを確認したらロジックを実装し再度テストを実行して期待する動作をするプログラムであることを保証する開発手法。

と書いてもTDDをやったことがない人には分かりにくいですよね。。。

そこで階乗を求めるという具体例を使って説明します。

階乗を求めるプログラム

階乗を求めるプログラムをTDDで実装してみます。

1. 関数のインターフェイス設計(引数と返り値の型を考える)

階乗は1からnまでの自然数の積で、以下のような数式で表されます。

n! = n * (n - 1) * (n - 2) * .... * 2 * 1

まずはこの式をどんな関数に落とし込むかを考えてみます。

  1. 計算したらint型の値を返して欲しい
  2. 引数にはint型の値nを入れる

この2つを盛り込むと int factorial(int n)という関数のインターフェイスが見えてきます。

2. 何もしない関数を作る

ではまず空の関数factorial()を実装してみます。

#include <stdio.h>

int factorial (int n) {
  return 0;
}

int main (void) {
  printf("%d\n", factorial(4));
  // => 0
  return 0;
}

上で考えた通りのインターフェイスの関数factorial()を実装しましたが、ロジック部分が空で常に0しか返しません。

これを実行してみると以下のようになります。

$ ./factorial
0

書いた通り0が出力されます。

3. assert()を使ってテストを書く(Red)

では次にassert()を使ってテストを書いてみます。

#include <stdio.h>
#include <assert.h>

int factorial (int n) {
  return 0;
}

void spec () {
  assert(factorial(4) == 24);
}

int main (void) {
  spec();
  printf("%d\n", factorial(4));
  // => 0
  return 0;
}

spec()関数内に書いたのがテストになります。ここでは4の階乗が24になることを期待します。

しかしこれを実行すると以下のようにエラーが出ます。

$ ./factorial
Assertion failed: (factorial(4) == 24), function spec, file a.c, line 9.
zsh: abort      ./factorial

factorial()関数は常に0を出力するので、これはつまり正常にエラーが出たということになります。

4. factorial()関数のロジックを埋める(Green)

ではfactorial()関数のロジックを埋めましょう。

#include <stdio.h>
#include <assert.h>

int factorial (int n) {
  if (n == 0) return 1;
  return n * factorial(n - 1);
}

void spec () {
  assert(factorial(4) == 24);
}

int main (void) {
  spec();
  printf("%d\n", factorial(4));
  // => 0
  return 0;
}

こうなります。(プログラムの説明は省略)

これを実行すると

$ ./factorial
24

となるように4の階乗がちゃんと24になります。

5. テストの改善

さて、次にテストケースを増やしていきます。

  1. 引数に0を入れたら正常に1が返るだろうか
  2. 引数に負の数を入れてもエラーは起きないだろうか

ざっと2つテストケースを挙げました。(他にも「intの最大値最小値を入れてみる」などありますが今回は省略)

これを実装すると以下のようになります。

#include <stdio.h>
#include <assert.h>

int factorial (int n) {
  if (n == 0) return 1;
  return n * factorial(n - 1);
}

void spec () {
  assert(factorial(4) == 24);
  assert(factorial(0) == 1);
  assert(factorial(-1) == 0);
}

int main (void) {
  spec();
  printf("%d\n", factorial(4));
  // => 0
  return 0;
}

これを実行すると、、、エラーが出てしまいます。

> ./factorial
zsh: segmentation fault  ./factorial

ああそうだ。引数が負の数の場合を実装していません。これではエラーが起きてしまいますね。

6. factorial()関数の改善(Refactoring)

factorial()関数に負の引数が入った場合に0を返すようにします。(階乗は0を返すことがないからホントはこれもあんまよくない)

#include <stdio.h>
#include <assert.h>

int factorial (int n) {
  if (n < 0) return 0;
  if (n == 0) return 1;
  return n * factorial(n - 1);
}

void spec () {
  assert(factorial(4) == 24);
  assert(factorial(0) == 1);
  assert(factorial(-1) == 0);
}

int main (void) {
  spec();
  printf("%d\n", factorial(4));
  // => 0
  return 0;
}

こうすると、

$ ./factorial
24

成功しました!

まとめ

今回はassert()を使ったテスト駆動開発C言語で行うことについて書きました。

ここでは分かりやすく階乗を例に出しましたがもっと複雑な状態になるプログラム(スタック、キューなど)だとかなり力を発揮してくれると思います!

C言語でのテスト駆動開発関連の本があります。