テストファーストプログラミング - 情報処理IIレジュメ(7)

1. はじめに

後期からの授業では複数人で一つのソースコードを共有して編集することがある。 複数人で共同してソフトウェアを書く場合には、皆で共通のコーディング規約を制定することが多い。 私の授業の範囲内では、moodle で配布したコーディング規約にしたがって,コードを書いてもらえると助かる。 もし、この規約に意見があるなら指摘して欲しい
(授業中ではここで規約の細かい説明を行う)。

2. テストファーストプログラミングとは

常にまずテストを書き、それを満足する関数を作成していくことを繰り返して、目的のプログラムを作成していく方法である。ステップごとに完成品ができ上がっていくために、プログラムを作る上で達成感があり、実装を変更した時のエンバグも少なくすることができる。

JAVA や Ruby などのオブジェクト思考言語では、テストファーストプログラムを簡単に実現するためのフレームワーク(Junit, TestUnit) などがある。今回の実験では、手続き型言語である C 言語を使用するため、フレームワーク部分までも実装する必要がある。

Windows の人は、C言語開発環境の構築(Windows) - BYOD PC のセッティング(24) - hkob’s blogの記事の一番下に書かれた方法を行い、PowerShell のフォントを「MS ゴシック」に変更しておくこと。

3. テストファーストプログラミングの例

ここでは、階乗の計算をする関数をテストファーストプログラミングを用いて作成する例を示す。プログラムの仕様は以下の通りである。

  • キーボードから整数 x を入力する
  • の階乗を計算する。x が 0 未満の場合にはエラーを表示する。
  • エラーでない場合には、計算した値を表示する

作成するプログラムは以下の通りである。たった一個の関数にしては、大がかりな仕掛けであるが、来週以降の時間関数群を作るときと同じ構造にしているためである。

  • factMain.c: 階乗を計算するプログラム本体。fact 関数を呼び出して結果を表示するだけのプログラム
  • fact.c: 階乗を計算する関数本体 fact 関数を記述する
  • fact.h: fact 関数のプロトタイプ宣言を記述する
  • testFact.c: fact.c をテストするための関数 testFact 関数を記述する

以下にプログラム作成の手順を示す。今回課題は以下の手順で示されるいくつか結果を記録し、各部分で起こっていることを考察することである。

3.1 リポジトリの準備

  1. 自分の作業したいフォルダに「fact」というフォルダを作成する
  2. http://bitbucket.org/ にログインし、「+」ボタンで「リポジトリ」を選択
  3. プロジェクトは「Untitle Project」を選択
  4. Name に「fact」と記述
  5. 「READMEを含めますか」を「No」とする
  6. 詳細を開き、プログラミング言語に「C」を選択
  7. ¥item 「リポジトリの作成」をクリック
  8. Clone in Sourcetree をクリック
  9. macOS の場合はダイアログが開くので、作業フォルダを先ほど作成した「fact」に変更し、Clone すればよい。Windows の人は SourceTree が立ち上がるだけなので、+で新しいタブを開き、Remote で表示される「fact」をclone する。この時、macOS と同様に作業するフォルダを上で作成した「fact 」フォルダにする。

3.2 必要なファイルのコピー

  1. SourceTree で Finder / Explorer をクリック
  2. moodle から「base.zip」をダウンロードする
  3. 8つのファイルを fact フォルダにコピーする
  4. このフォルダでターミナルやPowerShell を開く。macOS の場合には、FinderGo アイコンを押せばよい。Windows の人は「シフトキー」を押しながら右ボタンを押して、「このフォルダでPowerShell を開く」をクリックする。これらができない人は、ターミナルや PowerShell で「cd」コマンドを使って自分でフォルダを移動すること。
  5. Windows の人は、PowerShell で以下のコマンドを実行する。これは、表示する日本語の漢字コードを「UTF-8」に変更するものである。デフォルトでは sjis (932) なので文字化けしてしまうためである。
chcp 65001

3.3 エディタの起動とリポジトリの更新

  1. 開いたターミナルで「code .」としてフォルダを Visual Studio Code で開く
  2. 右の一覧で、「_gitignore+」ファイルの名前を「.gitignore+」に変える。
  3. すでにコンパイルに必要なファイルは全部作成済で、Makefile に記入済である。今後、別のプロジェクトを実施する時には、ファイル名の修正や Makefile の修正をここで実施する。
  4. SourceTree に戻ると 上記8つのファイルが現れるのでコミットしておく。今後は、作業がひと段落する度にコミットすること。

3.4 コンパイル自動実行環境を起動

コンパイルに必要なものは全て揃っているので、gnuplot の場合と同様に chokidar で自動実行環境を用意しておく。

chokidar *.c *.h Makefile -c "make test"

ここで適当なファイルを保存した時にコンパイルが動作するかを確認する。 Windows の場合だとこんな感じの画面が出てくる。必要なファイルがコンパイルされ、testFact.exe が実行される。今回一つだけ簡易テストを記述しておいたので、テストが100%成功していると表示されている。

Watching "*.c", "*.h", "Makefile" ..
change:Makefile
gcc -Wall -g   -c -o testFact.o testFact.c
gcc -Wall -g   -c -o fact.o fact.c
gcc -Wall -g   -c -o testCommon.o testCommon.c
gcc -o testFact.exe -Wall -g testFact.o fact.o  testCommon.o -lm
./testFact.exe
== Test abs ==
All tests are Ok. [# of Tests = 1, # of pass = 1 (100%)]

macOS の場合には、以下のようになる。

Watching "fact.c", "factMain.c", "testCommon.c", "testFact.c", "fact.h", "testCommon.h", "Makefile" ..
change:Makefile
cc -Wall -g   -c -o testFact.o testFact.c
cc -Wall -g   -c -o fact.o fact.c
cc -Wall -g   -c -o testCommon.o testCommon.c
cc -o testFact -Wall -g testFact.o fact.o  testCommon.o -lm
./testFact
== Test abs ==
All tests are Ok. [# of Tests = 1, # of pass = 1 (100%)]

ここで、SourceTree を開くと macOS の場合には、testFact という実行ファイルが候補に現れる。このファイルはリポジトリに登録すべきではないので、右ボタンで「無視...」で「このリポジトリのみ」で登録する。.gitginore が変更になるので、これをコミットしておく。Windows の場合には、「*.exe」が無理リストに入っているので特に何もやる必要はない。

このコンパイルおよびテスト実行結果を「結果1」として保存して、何が起こっているかを記録する。

3.5 最初のテスト

Visual Studio Code で testFact.c を開いてみる。以下の修正を行う。

  • 二箇所の testAbs を testFact に変更
  • 二箇所の abs を fact に変更
  • 関数の引数を -1 を 1 に変更
  • 「// 1 の階乗は 1 かどうか?」というコメントを追加しておく。
  • `#include "fact.h" を stdio.h の include の次の行に追加しておく。

変更後のファイルは以下のようになる。

#include <stdio.h>
#include "fact.h"
#include "testCommon.h"

void testFact() {
    testStart("fact");
    assertEqualsInt(fact(1), 1); // 1 の階乗は 1 かどうか?
}

int main() {
    testFact();
    testErrorCheck();
}

保存をすると、chokidar が make test を自動実行する。この時の結果を結果2として保存して、起こっていることを考察すること。

このなかで implicit declaration とは暗黙の定義という意味である。「知らないからとりあえず int をもらって int を返す関数だと勝手に判断したよ」ということになる。今回は正しいが、気持ち悪いので fact 関数のプロトタイプ宣言を fact.h の中に作る。この授業では関数のプロトタイプ宣言を考える時に、以下のような図を描くことにする。関数の戻り値(int)、関数名(fact)、引数(int x)がこの図で表現できていることを理解してほしい。

f:id:hkob:20200531102639p:plain
fact関数

上記の図が理解できれば、この図を翻訳してプロトタイプ宣言として「fact.h」に記載する。

/* fact.h by hkob */
int fact(int x); /* 一つの値を受けて、その値の階乗を返す */

この時の結果を結果3として保存して、起こっていることを考察すること(結果2との違いを確認すること)。

結果3 では「Undefined symbol」(macOS)や「Undefined reference」のような表示が出る。これは、プログラムのコンパイルは成功したが、fact という関数が見つからなかったというエラーである。これも作っていないから当たり前である。そこで、fact 関数の本体である fact 関数を fact.c の中に記述する。ただし、中身はまだ書かず、インターフェース部分である関数の引数の部分(と必要であればreturn文)のみを記述する。今回は、fact.c の中に以下のプログラムを記述する。

/* fact.c by hkob */

/* x の階乗を計算して返す */
int fact(int x) {
    return 0;
}

ここでは、整数型変数 x を入力し、整数型を返す関数 fact を作成した。基本的には関数の雛形はプロトタイプ宣言の「;」を「{}」に変えるだけである。また、この関数は int の値を返す必要があるので、とりあえず return 0; を記述している。void 関数の場合には、return は書かなくてよい。テストファーストプログラミングでは、関数の呼び出しだけが成功するかを最初に確認する。最初は決して中身を書かないこと。

この時の結果を結果4として保存して、起こっていることを考察すること。

この時、testFact ... Error: a != b (0 != 1)という出力が確認できれば成功である。これは、1 の階乗を計算する関数が 0 を返していてテストに失敗しているためである。このテストプログラムが動作し、最初にテストに失敗することがテストファーストプログラミングで必要なことである。

3.6 実装の記述(つじつま合わせの成功)

とりあえず結果を合わせるために、fact 関数の return 0; を return 1; に直してコンパイルし、実行してみる。この時の結果を結果5として保存して、起こっていることを考察すること。

テストが通ったのが確認できる。このようにひとまずはテストが通ることを目標に簡単なプログラムを作り、それを修正していくことで関数を拡張していく。

3.7 テストを追加(つじつま合わせではうまくいかない)

気をよくしてどんどんテストを追加してみる(2 の階乗、3の階乗、6の階乗を追加する)

    assertEqualsInt(fact(1), 1); // 1 の階乗は 1 かどうか?
    assertEqualsInt(fact(2), 2); // この行を追加
    assertEqualsInt(fact(3), 6); // この行を追加 
    assertEqualsInt(fact(6), 720); // この行を追加

この時の結果を結果6として保存して、起こっていることを考察すること。

これらのテストが通過するように fact.c を直してみる(ここは自分で頑張ること)。成功した時の結果を記録しておくこと。成功した時の結果を結果7として保存して、起こっていることを考察すること。

3.8 0の階乗のテストの追加、負の階乗のテストの追加

以下のテストを追加して、テストが通るように fact.c を直してみる。0 の階乗は 1 であるというテストである。また、負の数を入れた場合も同様にテストしてみる。負の値は定義されていないので、とりあえず -1 を返すなど自分で工夫すること。

    assertEqualsInt(fact(0), 1);
    assertEqualsInt(fact(-1), 自分で決めて);
    assertEqualsInt(fact(-3), 自分で決めて);

この時の結果を結果8として保存して、起こっていることを考察すること。

テストが通るように実装を記述する。成功した時の結果も記録しておくこと。 この時の結果を結果9として保存して、起こっていることを考察すること。

3.9 関数を信用し、他のプログラムで使用する

ここまでテストすれば fact.c は完成したと思われる。最後にメインプログラムを書いてみる

/* factMain.c by hkob */
#include <stdio.h>
#include "fact.h"

int main() {
    int x, y;
    fprintf(stderr, "整数を一つ入力して下さい");
    scanf("%d", &x);
    y = fact(x);
    if (自分で定めた条件) {
        printf("0 未満の階乗は計算できません¥n");
    } else {
        printf("%d の階乗は %d です¥n", x, y);
    }
    return 0;
}

4. 授業のまとめ

以上の手順をまとめると以下のようになる。

  1. 関数のテスト関数を作成する
  2. コンパイルする。→ コンパイルエラー(関数の正体をしらないから)
  3. 関数のプロトタイプ宣言を記述する
  4. コンパイルする。→ リンクエラー (関数の正体はわかったが、関数自体がない)
  5. 関数のインターフェースを記述する
  6. コンパイルする。→ コンパイル成功。実行時にテストでエラー(何もしない関数だから、結果も合わないはず)
  7. とりあえずつじつま合わせの実装でもいいから記述して、テストを通過させる
  8. コンパイル・実行する → テスト成功。ただし、つじつま合わせなのですべてでうまく行くとは限らない
  9. 偶然うまくいっているだけかもしれないので、テストを追加してみる。
  10. コンパイル・実行する → さまざまな組み合わせでうまく言ったのなら、この関数は信用できるから使い回そう。

hkob.hatenablog.com