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

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

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

繰り返されるinput系の修正

やりかけだった,inputbufferの続きです。

リスト10

class   inputbuffer {
        var     $linelist;
        function        inputbuffer() {
                $this->linelist = array();
        }
        function        iseof() {
                if (count($this->linelist) == 0)
                        return 1;
                return 0;
        }
        function        addcontents($contents, $name = "(unknown)", $startnumber = 1) {
                $a = array();
                foreach (split("\r|\n|\r\n", $contents) as $s)
                        $a[] =& new inputline($s, $name." #".($startnumber++));
                array_splice($this->linelist, 0, 0, $a);
        }
        function        &getline() {
                $obj =& $this->linelist[0];
                array_shift($this->linelist);
                return $obj;
        }
        function        test1() {
                $obj =& new inputbuffer();
                assertisa("inputbuffer", $obj)
                        ->iseof_eqf(1);
                
                $obj->addcontents("test1\ntest2\n"); /* (1) */
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("test1");
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("test2");
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("");
                
                $obj->addcontents("test3\ntest4\n");
                asserteq(0, $obj->iseof());
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("test3");
                asserteq(0, $obj->iseof());
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("test4");
                asserteq(0, $obj->iseof());
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("");
                asserteq(1, $obj->iseof());
                
                $obj->addcontents("test5\ntest6\n", "testfile.txt");
                assertisa("inputline", $obj->getline())
                        ->getlocate_eqf("testfile.txt #1");
                assertisa("inputline", $obj->getline())
                        ->getlocate_eqf("testfile.txt #2");
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("");
                
                $obj->addcontents("test7\ntest8");
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("test7");
                $obj->addcontents("test9\ntest10");  /* (2) */
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("test9");
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("test10");
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("test8");
                asserteq(1, $obj->iseof());
                
        }
}

(1)以降で,ファイル名と行番号を,先に作ったinputlineのlocateに設定しています。

いったんaddcontents()のコードを書きますが,順番が逆になるのに気づいて,(2)で修正しました。これは,読み込んだ行がincludeなどであった場合,現在行の直後に読み込んだファイルが入るというのを想定したものです。

さて,コメントの処理は,inputlineの後で行っていました。しかし,コメントの除去はあちこちで行われるため,commentinputlineで一括して行う方式に変更しました。

リスト11

class   commentlessinputline extends inputline {
        function        commentlessinputline($line = "", $locate = "(unknown)") {
                if (($pos = strpos($line, ";")) !== FALSE)
                        $line = substr($line, 0, $pos);
                parent::inputline($line, $locate);
        }
        function        test1() {
                $obj =& new commentlessinputline("test");
                assertisa("inputline", $obj)
                        ->gets_eqf("test");
                $obj =& new commentlessinputline("test2");
                assertisa("inputline", $obj)
                        ->gets_eqf("test2");
                
                $obj =& new commentlessinputline("test3", "line#1");
                assertisa("inputline", $obj)
                        ->gets_eqf("test3")
                        ->getlocate_eqf("line#1");
                $obj =& new commentlessinputline("Hello TDD", "line#2");
                assertisa("inputline", $obj)
                        ->gets_eqf("Hello TDD")
                        ->getlocate_eqf("line#2");
                
                $obj =& new commentlessinputline("test");
                assertisa("inputline", $obj)
                        ->gets_eqf("test")
                        ->getlocate_eqf("(unknown)");
                
                $obj =& new commentlessinputline("this is a test.");
                assertisa("inputline", $obj)
                        ->gets_eqf("this is a test.")
                        ->gets_eqf(null);
                
                $obj->ungets("abc");
                assertisa("inputline", $obj)
                        ->gets_eqf("abc");
                
                $obj->ungets("def");
                $obj->ungets("ghi");
                assertisa("inputline", $obj)
                        ->gets_eqf("ghidef")
                        ->gets_eqf(null);
                
                $obj =& new commentlessinputline("label: decfsz INDF, 1 ; comment");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("decfsz INDF, 1 ", $obj->gets());
                
                $obj =& new commentlessinputline("label decfsz INDF, 1 ; comment");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("decfsz INDF, 1 ", $obj->gets());
                
                $obj =& new commentlessinputline(" decfsz INDF, 1 ; comment");
                asserteq("", $obj->getchunk(": \t"));
                asserteq("decfsz INDF, 1 ", $obj->gets());
                
                $obj =& new commentlessinputline("label:");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq(null, $obj->gets());
                
                $obj =& new commentlessinputline("label");
                asserteq("label", $obj->getchunk(": \t", 1));
                asserteq(null, $obj->gets());
                
                $obj =& new commentlessinputline("label: decfsz INDF, 1 ; comment");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("decfsz", $obj->getchunk());
                asserteq("INDF, 1 ", $obj->gets());
                
                $obj =& new commentlessinputline("label: clrwdt");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("clrwdt", $obj->getchunk());
                asserteq(null, $obj->gets());
                
                $obj =& new commentlessinputline("label: decfsz INDF, 1 ; comment");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("decfsz", $obj->getchunk());
                asserteq("INDF", $obj->getchunk(","));
                asserteq("1 ", $obj->gets());
                
                $obj =& new commentlessinputline("label: decfsz INDF");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("decfsz", $obj->getchunk());
                asserteq(null, $obj->getchunk(","));
                asserteq("INDF", $obj->gets());
                
                $obj =& new commentlessinputline("label\tdecfsz\tINDF\t,\t1\t;\tcomment");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("decfsz", $obj->getchunk());
                asserteq("INDF", $obj->getchunk(","));
                asserteq("1", $obj->getchunk());
                asserteq(null, $obj->gets());
                
        }
}


class   inputbuffer {

/* .... */

        function        addcontents($contents, $name = "(unknown)", $startnumber = 1) {
                $a = array();
                foreach (split("\r|\n|\r\n", $contents) as $s)
                        $a[] =& new commentlessinputline($s, $name." #".($startnumber++));
                array_splice($this->linelist, 0, 0, $a);
        }

/* .... */

}

commentlessinputlineを作ったあと,inputlineのテストをすべてcommentlessinputlineに修正しています。

その後,いろいろ迷ったのですが,commentlessinputlineをやめて,commentlessinputbufferを採用しました。これは,もしcommentlessでないinputlineが必要になったときに,inputbufferの中を書き換えないといけないのを嫌ったためです。

リスト12

class   commentlessinputbuffer extends inputbuffer {
        function        addcontents($contents, $name = "(unknown)", $startnumber = 1) {
                $a = array();
                foreach (split("\r|\n|\r\n", $contents) as $s) {
                        if (($pos = strpos($s, ";")) !== FALSE)
                                $s = substr($s, 0, $pos);
                        $a[] =& new inputline($s, $name." #".($startnumber++));
                }
                array_splice($this->linelist, 0, 0, $a);
        }
        function        test1() {
                $obj =& new commentlessinputbuffer();
                $obj->addcontents("line;comment");
                assertisa("inputline", $obj->getline())
                        ->gets_eqf("line");
        }
}

コメントが取り除かれることを検証するテストを書いたあと,先ほどのcommentlessinputlineの修正を元に戻します。これまでのテストも,コメント処理まわりを取り除き,クラス名の変更に対応しています。

このあとアセンブラのメインの処理を書いていくのですが,そのときにまた修正が必要になりました。

入力にエラーがあってアセンブル処理を継続できない場合は,エラーを表示する必要があります。しかし,エラーを画面に表示してdie()などで終了してしまうと,テストが継続できなくなります。テストでは「きちんとエラーになって処理が止まるか」のようなことを検証しないといけません。画面に表示されたり,ましてや終了されてしまうとテストに差し障りがあります。

リスト13

class	inputbuffer {
        var     $linelist;
        var     $message = null;

/* .... */

        function        getmessage() {
                if (count($this->message) <= 0)
                        return null;
                return implode("\n", $this->message);
        }
        function        addmessage($s = "") {
                $this->message[] = $s;
        }
        function        test1() {

/* .... */

                $obj =& new inputbuffer();  /* (1) */
                asserteq(null, $obj->getmessage());
                $obj->addmessage("error1");
                asserteq("error1", $obj->getmessage());
                $obj->addmessage("error2"); /* (2) */
                asserteq("error1\nerror2", $obj->getmessage());
                
        }
}

そこで,(1)でinputbufferにaddmessage()というメソッドを用意します。エラーが発生したら,ここにメッセージを書き込みます。(2)で,エラーが2回以上発生した場合は改行で連結されるようにしています。

アセンブル処理本体に渡るのは,inputbufferではなくinputlineです。

リスト14

class   inputline {
        var     $line = null;
        var     $locate = "";
        var     $parent = null;

/* .... */

        function        setparent(&$parent) {
                $this->parent =& $parent;
        }
        function        addmessage($message = "") {
                if ($this->parent === null)
                        return;
                $this->parent->addmessage($message." in ".$this->locate);
        }
        function        test1() {

/* .... */

                $obj =& new inputline(); /* (1) */
                asserteq(null, $obj->parent);
                $obj2 =& new inputline();
                $obj->setparent($obj2);
                asserteq($obj2, $obj->parent);
                
                $obj =& new inputline();
                $obj->addmessage("dummy");
                
        }
}


class   inputbuffer {

/* .... */

        function        test1() {

/* .... */

                $obj =& new inputbuffer();
                asserteq(null, $obj->getmessage());
                $obj->addmessage("error1");
                asserteq("error1", $obj->getmessage());
                $obj->addmessage("error2");
                asserteq("error1\nerror2", $obj->getmessage());
                
                $obj->addcontents("test9", "testfile2.txt");  /* (2) */
                $line =& $obj->getline();
                asserteq($obj, $line->parent);
                $line->addmessage("error3");
                asserteq("error1\nerror2\nerror3 in testfile2.txt #1", $obj->getmessage());
                
                $obj->addcontents("test10\ntest11", "testfile3.txt");
                $line =& $obj->getline();
                $line =& $obj->getline();
                $line->addmessage("error4");
                asserteq("error1\nerror2\nerror3 in testfile2.txt #1\nerror4 in testfile3.txt #2", $obj->getmessage());
                
        }
}

(1)で,inputline->parentにinputbufferがセットされるようにします。(2)で,inputlineにもaddmessage()を用意し,ここでファイル名や行番号を付加して,inputbuffer->addmessage()を呼ぶようにします。

さて,こうして不十分な設計による典型的な混乱のサンプルが得られました。これを元に,テスト駆動開発の疑問点を見ていくことにしましょう。

著者プロフィール

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

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

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