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

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

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

トークンへの分解の方法

さて,ここで問題があります。行をラベル・オペコード・オペランドなどに分けるのは,どこでやるのが良いでしょう。inputlineの中に機能を追加するべきでしょうか。それとも,読みとったあとでやるのがいいでしょうか。

これもいくつか考えられますが,⁠指定した区切り文字まで読み取る」という機能をinputlineに用意することにしました。このために,まずgets()で文字列を取りだすと,文字バッファをクリアするように変更しました。

リスト5

class   inputline {
        var     $line = "";
        var     $locate = "";
        function        inputline($line = "", $locate = "(unknown)") {
                $this->line = $line;
                $this->locate = $locate;
        }
        function        gets() {
                $ret = $this->line;
                $this->line = "";
                return $ret;
        }
        function        ungets($s = "") {
                $this->line = $s.$this->line;
        }
        function        getlocate() {
                return $this->locate;
        }
        function        test1() {

/* .... */

                $obj =& new inputline("this is a test.");  /* (1) */
                assertisa("inputline", $obj)
                        ->gets_eqf("this is a test.")
                        ->gets_eqf("");
                
                $obj->ungets("abc");    /* (2) */
                assertisa("inputline", $obj)
                        ->gets_eqf("abc");
                
        }
}

(1)で,2回目のgets()が""になることをテストします。テストに失敗しますので,コードを追加します。(2)で,ungets()をテストしています。gets()を呼ぶと文字列が失われてしまうようになりましたので,もう一度読めるようにするには,ungets()を利用します。

リスト6

class   inputline {
        var     $line = null;
        var     $locate = "";
        function        inputline($line = "", $locate = "(unknown)") {
                $this->line = $line;
                $this->locate = $locate;
        }
        function        gets() {
                $ret = $this->line;
                $this->line = null;
                return $ret;
        }
        function        ungets($s = "") {
                $this->line = $s.$this->line;
        }
        function        getlocate() {
                return $this->locate;
        }
        function        test1() {

/* .... */

                $obj =& new inputline("this is a test.");
                assertisa("inputline", $obj)
                        ->gets_eqf("this is a test.")
                        ->gets_eqf(null);  /* (4) */
                
                $obj->ungets("abc");
                assertisa("inputline", $obj)
                        ->gets_eqf("abc");
                
                $obj->ungets("def");  /* (3) */
                $obj->ungets("ghi");
                assertisa("inputline", $obj)
                        ->gets_eqf("ghidef")
                        ->gets_eqf(null);  /* (4) */
                
        }
}

(3)では,2回ungetした場合の動作をテストしています。(4)は,空行と,gets()後を区別するため,gets()の返り値をnullに変更したときのテストです。(1)のテストにも変更が発生しました。

続いて,getchunk()を作ります。getchunk()は,どう処理するか決められなくて,何度も変更を行った箇所です。テストが増えるようすを見ていきましょう。

リスト7

class	inputline {

/* .... */

        function        getchunk($terminator = "", $terminator2 = "") {
                $line = $this->gets();
                $ret = "";
                for (;;) {
                        if ($line == "")
                                return null;
                        $c = substr($line, 0, 1);
                        $line = substr($line, 1);
                        if (strpos($terminator, $c) !== FALSE)
                                break;
                        $ret .= $c;
                }
                while ($line != "") {
                        $c = substr($line, 0, 1);
                        if (strpos($terminator, $c) === FALSE)
                                break;
                        $line = substr($line, 1);
                }
                $this->ungets($line);
                return $ret;
        }

/* .... */

        function        test1() {

/* .... */

                $obj =& new inputline("label: decfsz INDF, 1 ; comment");  /* (1) */
                asserteq("label", $obj->getchunk(": "));
                asserteq("decfsz INDF, 1 ; comment", $obj->gets());  /* (2) */
                
        }
}

(1)では,典型的なアセンブラの1行を入力して,先頭のラベルが取得できることをテストしています。もちろんエラーになりますので,コードを書いてテストを通します。(2)で,続くオペコードが取得できるかどうかテストしています。

この段階のgetchunk()では,2つの引数を受け付けます。1つ目は区切り文字で,2つ目は区切り文字のあとの文字を読み飛ばすための指定です。これはアセンブラの構文によるものです。ラベルは必ず先頭に書き,ラベルがない場合はタブを入れる,ラベルの末尾に「:」があってもよい,という処理のため,⁠label-タブ-opcode」の場合は"label"が,⁠タブ-opcode」の場合は""が読みとられるようにしたかったためです。

リスト8

class	inputline {

/* .... */

        function        getchunk($terminator = "", $eolenable = 0) {  /* (4) */
                $line = $this->gets();
                $ret = "";
                for (;;) {
                        if ($line == "") {
                                if (($eolenable))
                                        return $ret;
                                $this->ungets($ret);
                                return null;
                        }
                        $c = substr($line, 0, 1);
                        $line = substr($line, 1);
                        if (strpos($terminator, $c) !== FALSE)
                                break;
                        $ret .= $c;
                }
                while ($line != "") {
                        $c = substr($line, 0, 1);
                        if (strpos(" \t", $c) === FALSE)
                                break;
                        $line = substr($line, 1);
                }
                $this->ungets($line);
                return $ret;
        }

/* .... */

        function        test1() {

/* .... */

                $obj =& new inputline("label: decfsz INDF, 1 ; comment");
                asserteq("label", $obj->getchunk(": \t"));  /* (5) */
                asserteq("decfsz INDF, 1 ; comment", $obj->gets());
                
                $obj =& new inputline("label decfsz INDF, 1 ; comment");  /* (3) */
                asserteq("label", $obj->getchunk(": \t")); /* (5) */
                asserteq("decfsz INDF, 1 ; comment", $obj->gets());
                
                $obj =& new inputline(" decfsz INDF, 1 ; comment");
                asserteq("", $obj->getchunk(": \t"));   /* (5) */
                asserteq("decfsz INDF, 1 ; comment", $obj->gets());
                
                $obj =& new inputline("label:");
                asserteq("label", $obj->getchunk(": \t"));  /* (5) */
                asserteq("", $obj->gets());
                
                $obj =& new inputline("label");
                asserteq("label", $obj->getchunk(": \t", 1));  /* (5) */
                asserteq(null, $obj->gets());
                
        }
}

(3)では,ラベルに「:」がない場合,ラベルそのものがない場合,ラベルだけでコード部がない場合などをテストしています。コードを書き,区切り文字なしに行末に到達しても正常と見なすかを指定しないといけないことに気づきます。(4)でメソッドの引数を変更して対応し,それに合わせて(5)で今までのテストを修正しています。

リスト9

class	inputline {

/* .... */

        function        getchunk($terminator = null, $eolenable = 0) {
                if ($terminator === null) {
                        $terminator = " \t";
                        $eolenable = 1;
                }
                
                $line = $this->gets();
                $ret = "";
                for (;;) {
                        if ($line == "") {
                                if (($eolenable))
                                        return $ret;
                                $this->ungets($ret);
                                return null;
                        }
                        $c = substr($line, 0, 1);
                        $line = substr($line, 1);
                        if (strpos($terminator, $c) !== FALSE)
                                break;
                        $ret .= $c;
                }
                while ($line != "") {
                        $c = substr($line, 0, 1);
                        if (strpos(" \t", $c) === FALSE)
                                break;
                        $line = substr($line, 1);
                }
                $this->ungets($line);
                return trim($ret);
        }

/* .... */

        function        test1() {

/* .... */

                $obj =& new inputline("label: decfsz INDF, 1 ; comment");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("decfsz", $obj->getchunk()); /* (6) */
                asserteq("INDF, 1 ; comment", $obj->gets());
                
                $obj =& new inputline("label: clrwdt");  /* (7) */
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("clrwdt", $obj->getchunk());
                asserteq(null, $obj->gets());
                
                $obj =& new inputline("label: decfsz INDF, 1 ; comment");
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("decfsz", $obj->getchunk());
                asserteq("INDF", $obj->getchunk(","));
                asserteq("1 ; comment", $obj->gets());
                
                $obj =& new inputline("label: decfsz INDF"); /* (8) */
                asserteq("label", $obj->getchunk(": \t"));
                asserteq("decfsz", $obj->getchunk());
                asserteq(null, $obj->getchunk(","));
                asserteq("INDF", $obj->gets());
                
                $obj =& new inputline("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(";\tcomment", $obj->gets());
                
        }
}

(6)で,区切り文字を省略した場合の動作をテストしています。(7)は,引数がないアセンブラ命令をテストしています。

(8)で,コメントがない場合,空白でなくてタブの場合などをテストしています。このテスト,区切り文字の前に空白などがあった場合に取り除く処理を追加しています。

実は,入力まわりの変更はここで終わりません。変更を続けても,テスト駆動開発が機能するのかどうか,見てみましょう。

著者プロフィール

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

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

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