はじめに
いろいろあって,かっこいいブログを作ることができたので,早速なんか書いていこうと思います。今回はテストについてです。
テストファースト
ソフトウェア開発において,プログラマはソフトウェアの部品となるデータ構造や関数を一つづつ作っていくという作業をします。この時,関数やデータ構造にはそれぞれ期待される役割を持っています。例えば,int abs(int x)
という関数は,絶対値を返すという機能を持っていることを期待されます。このような期待される役割を機能要件と呼ぶこともあります。
機能要件が定まっていたとしても,それだけでプログラムを書いていくのは難しいことが多いと思います。ソフトウェアの期待される機能を実現するには,それを複数の部品に分割していく必要もあるでしょう。このような作業はソフトウェアの設計に含まれます。
ソフトウェアの設計が終わって,分割された一つの関数を実装するという段階になってもまだどのように実装すればいいかわからないということがあります 。このような場合にはどうしたら良いでしょうか。
一つの方法として,まずテストを書くということから始めることができます。関数が果たすべき役割がある程度明確になっていれば,その関数が果たす仕事の典型的なケースについては列挙することができるはずだからです。これができないということは,関数の典型的な振る舞いさえも明らかではない,ということであり,もう一度設計の段階に戻って考えることになります。例えば,abs(x)
で言うと以下のようなテストを書くことができます。
#include <stdbool.h>
#include <stdio.h>
int my_abs(int x);
void test(void)
{
bool result = (my_abs(-100) == 100) // -100の絶対値は100と期待される
if (result)
puts("ok");
else
puts("ko");
}
int main(void)
{
test();
}
では,このコードが書かれたファイルをtest.c
という名前で保存して,コンパイルしてみましょう。結果は以下のようになります。
あらまぁ,コンパイルできてないみたいですね。なんででしょうか…
はい,答えは簡単で,my_abs
という関数がまだないからですね。test.c
にmy_abs
の実装を追加しましょう。こんな感じでしょうかね。
int my_abs(int x)
{
return(-x);
}
コンパイルして,実行してみます。
わーい,okだ!
okじゃない
しかし,abs
の典型的な場合とは他にもあります。入力が正の整数の時には,abs
は,その数をそのまま返さなくてはなりません。この振る舞いについてもテストしましょう。
#include <stdbool.h>
#include <stdio.h>
int my_abs(int x)
{
return (-x);
}
void test(void)
{
bool result = (my_abs(-100) == 100) && (my_abs(100) == 100);
if (result)
puts("ok");
else
puts("ko");
}
int main(void)
{
test();
}
結果は…
koでした。
入力が正の時と負の時で分岐するべきだったんですね。実装を修正しましょう。
int my_abs(int x)
{
if (x < 0)
return (-x);
else
return (x);
}
行ったっしょ!
はい,完璧!
完璧じゃない
典型的な振る舞いはこれでできたように思いますが,これでは完璧とまでは言えません。ここで完璧な関数とは,全ての場合について期待される振る舞いをするような関数です。
ここで,テストするべき入力値は,典型的ではない値です。典型的でない値の典型(?)としては,0
値や,最大値,最小値があります。int
型の最小値のテストを追加しましょう。
void test(void)
{
bool result = (my_abs(-100) == 100) && (my_abs(100) == 100);
bool special = (my_abs(-2147483648) == 2147483648);
if (result && special)
puts("ok");
else
puts("ko");
}
結果は以下のようになります。
koやね。
理由は,int
型の最小値の絶対値は,int
型の最大値よりも1だけ大きいからです。なぜそうなのかということについては…うん,えーと2の補数表現がね…あれなのよ…
…宿題にさせてください。
ということで,-2147483648
の絶対値を返すことはできないことがわかりました。これはつまり,-2147483648
は例外的な入力値であるということです。従って,実装で,-2147483648
の場合は例外的に扱うことにしましょう。
int my_abs(int x)
{
if (-2147483648 == x)
return (-1);
if (x < 0)
return (-x);
else
return (x);
}
通常,この関数は正の値を返すため,my_abs
が -1
を返した場合は,入力が対応していない場合を意味することができます。テストを修正しましょう。
void test(void)
{
bool result = (my_abs(-100) == 100) && (my_abs(100) == 100);
bool special = (my_abs(-2147483648) == -1);
if (result && special)
puts("ok");
else
puts("ko");
}
結果は…
okやね😊
このように,テストを書いていく中で,関数の機能要件が段々と明確になっていくことがあります。これを,段階的詳細化と言います。
では,他の非典型的な値(の典型)についても,テストを追加しましょう。以下のようになります。
#include <limits.h>
...
bool special = (my_abs(INT_MIN) == -1) && \
(my_abs(INT_MAX) == INT_MAX) && \
(my_abs(0) == 0);
結果は…
完璧やん!
おわりに
はい,ということでテストファーストで関数を作っていくやり方を紹介しました。これからこのやり方を拡張していくことに関するブログ記事も書きたいと思います。また,ソフトウェアの設計に関することも勉強して,ブログ記事としてアウトプットしていきたいなと思います。最後まで読んでいただきありがとうございました!