テストを“いちばん重要な財産”と考えると見えるもの

第3回 テスト駆動は“オブジェクト指向にあとから変更”にも対応できるのか

この記事を読むのに必要な時間:およそ 14 分

大変お待たせしました。遅ればせながら本年もよろしくお願いします。さっそく,テスト駆動開発の続きに取り組んでいきましょう。

筆者はテスト駆動開発に興味を持ってはいたのですが,ずっと取り込んできませんでした。コンセプトは魅力的だと思うのですが,半信半疑なところもあったためです。

  • 複雑なものも扱えるのか
  • 生産性が本当に上がるのか
  • 部分をテストしても全体のテストのかわりにはならないのでは?
  • テスト可能にすると,ソースが読みにくくなるのでは?
  • 最初に構造をしっかり設計しないと,あとから変更できないのでは?

特に最後の「最初に構造をしっかり設計しないといけない」は,筆者がオブジェクト指向言語を使うようになって感じた点です。オブジェクト指向を使わないよりも大きなものが作れるのですが,そのかわり当初の仮定が間違っていたときの軌道修正が難しいと感じました。

そこで,今回はこういった疑問について検証していこうと思います。どうぞよろしくお付き合いください。

わざと設計なしでプログラムを書く

使用するのはPHPでアセンブラを作ってみたという記事です。このコードはオブジェクト指向を使っていないため,完全に書き直す必要があります。しかも今回は,初期の設計を重視せず,わざと思いつきでコードを書いていきました。あとからの大変更も何度も発生しています。このため,先述の疑問に答えるにはぴったり,というわけです。

さて,オブジェクト指向化するといっても,何かゴールを設定しないと方針が決められません。今回はアセンブラですので,以下の問題の解消をゴールにしてみました。

  • エラー発生時の行番号表示
  • 前方参照をきれいに実装できる

「前方参照」「後方参照」とも言い,まだ出現していないラベルを参照する,という意味です。つまり,ソースの上の方に「goto main」とあり,ソースの下の方に「main:」とあるようなケースです。この場合,上から1行ずつ見ていくと,⁠goto main」の段階では「main」のラベルは解決できないことになります。これをエレガントに解決したい,というわけです。

また,今回はテストエンジンのrun.phpも機能を追加しました。

  • テストに失敗した場合でも,次のテストメソッドを実行する(Exceptionを使用しないバージョン)
  • クラスでない,普通の関数もテスト可能
  • オブジェクトのメンバを少ない記述でテストできる機能(php5のマジックメソッドを使用)

オブジェクトのテストだけ,簡単に説明します。assertisa("class1", $obj)は,$objがclass1(またはそのサブクラス)であることをテストします。このとき,テスト用のオブジェクトが得られ,メンバ変数や,引数を持たないメソッドの返り値のテストができるようになっています。

 $obj =& new class1();
 assertisa("class1", $obj)
  ->var1_eq(1)
  ->method1_eqf(2);

var1_eq(1)のように_eqがついていると,$obj->var1が1であるかどうかテストします。つまり,asserteq(1, $obj->var1)と同じということです。そして,$thisを返しますので,続けて別のテストを行うことができます。method1_eqf(2)のように_eqfがついていると,$obj->method1()が2であるかどうかテストします。これを利用して,いくつものメンバ変数やメソッドを見通しよくテストすることができます。

紙面の都合上,テストエンジンの解説は次回を予定しています。複雑なものではありませんので,興味のある方はソースをご覧ください。

行番号のためのクラスを作る

さっそくですが,ここからはどのようにソースの構造(アーキテクチャ)が変化していったかにご注目ください。前回のソースはこちらです。今回は,何度も変更が行われたinputlineとinputbufferまわりに注目します。これは,行番号を扱うために用意したクラスです。

おさらいになりますが,テスト駆動開発では,最初にテストを1つ書きます。そして,そのテストをクリアするための,最低限のコードを書くのでしたね。したがって,最初の状態のソースはこうなります。

リスト1

class   inputbuffer {
        function        test1() {
                $obj =& new inputbuffer();
                assertisa("inputbuffer", $obj)
                        ->iseof_eqf(1);
        }
}

空のinputbufferを作り,iseof()をテストしています。ソースコードの処理では,inputbufferから行を読み取ることで,行番号の情報も得られるようにしよう,という魂胆です。iseof()がなくてエラーになるのでコードを書きます。

リスト2

class   inputbuffer {
        function        iseof() {
                return 1;
        }
        function        test1() {
                $obj =& new inputbuffer();
                assertisa("inputbuffer", $obj)
                        ->iseof_eqf(1);
        }
}

しかし,ここまで書いたところで,必要なのは行番号だと気づきました。includeなどを考えると,ファイル名も必要になる可能性があります。行をオブジェクトにした方がいいかも知れません。そこでinputbufferは書きかけのままにして,inputlineの作業に取りかかりました。

以下のように,テストを書いていきます。

リスト3

class   inputline {
        var     $line = "";
        function        inputline($line = "") {
                $this->line = $line;
        }
        function        gets() {
                return $this->line;
        }
        function        test1() {
                $obj =& new inputline("test");  /* (1) */
                assertisa("inputline", $obj)
                        ->gets_eqf("test");
                $obj =& new inputline("test2"); /* (2) */
                assertisa("inputline", $obj)
                        ->gets_eqf("test2");
        }
}

(1)で,コンストラクタに渡した文字列が,gets()で取得できることを確認します。もちろんエラーになるので,インチキコードを書いてテストを通します。(2)で別の文字列を使ってテストし,インチキコードを直します。

リスト4

class   inputline {
        var     $line = "";
        var     $locate = "";
        function        inputline($line = "", $locate = "(unknown)") {
                $this->line = $line;
                $this->locate = $locate;
        }
        function        gets() {
                return $this->line;
        }
        function        getlocate() {
                return $this->locate;
        }
        function        test1() {
                $obj =& new inputline("test");
                assertisa("inputline", $obj)
                        ->gets_eqf("test");
                $obj =& new inputline("test2");
                assertisa("inputline", $obj)
                        ->gets_eqf("test2");
                
                $obj =& new inputline("test3", "line#1"); /* (3) */
                assertisa("inputline", $obj)
                        ->gets_eqf("test3")
                        ->getlocate_eqf("line#1");
                $obj =& new inputline("Hello TDD", "line#2"); /* (4) */
                assertisa("inputline", $obj)
                        ->gets_eqf("Hello TDD")
                        ->getlocate_eqf("line#2");
                
                $obj =& new inputline("test");   /* (5) */
                assertisa("inputline", $obj)
                        ->gets_eqf("test")
                        ->getlocate_eqf("(unknown)");
        }
}

(3)で行番号の情報をテストします。今回は,好きな文字列を渡せるようにしました。(4)で別の行番号をテストします。(5)では,行番号を指定しなかった場合のディフォールト値をテストしています。

著者プロフィール

木元峰之(きもとみねゆき)

独立系ソフトハウスに8年間勤務,パッケージソフトの開発や記事執筆などを行う。現在はフリーのコンサルタント。SWESTなどのワークショップで分科会のコーディネータを務める。デジタル回路設計歴30年,プログラミング歴27年。

きもと特急電子設計
URL:http://business.pa-i.org/

コメント

コメントの記入