大変お待たせしました。遅ればせながら本年もよろしくお願いします。さっそく、
筆者はテスト駆動開発に興味を持ってはいたのですが、
- 複雑なものも扱えるのか
- 生産性が本当に上がるのか
- 部分をテストしても全体のテストのかわりにはならないのでは?
- テスト可能にすると、
ソースが読みにくくなるのでは? - 最初に構造をしっかり設計しないと、
あとから変更できないのでは?
特に最後の
そこで、
わざと設計なしでプログラムを書く
使用するのは
さて、
- エラー発生時の行番号表示
- 前方参照をきれいに実装できる
「前方参照」
また、
- テストに失敗した場合でも、
次のテストメソッドを実行する (Exceptionを使用しないバージョン) - クラスでない、
普通の関数もテスト可能 - オブジェクトのメンバを少ない記述でテストできる機能
(php5のマジックメソッドを使用)
オブジェクトのテストだけ、
$obj =& new class1();
assertisa("class1", $obj)
->var1_eq(1)
->method1_eqf(2);
var1_
紙面の都合上、
行番号のためのクラスを作る
さっそくですが、
おさらいになりますが、
class inputbuffer {
function test1() {
$obj =& new inputbuffer();
assertisa("inputbuffer", $obj)
->iseof_eqf(1);
}
}
空のinputbufferを作り、
class inputbuffer {
function iseof() {
return 1;
}
function test1() {
$obj =& new inputbuffer();
assertisa("inputbuffer", $obj)
->iseof_eqf(1);
}
}
しかし、
以下のように、
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)で、
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)で行番号の情報をテストします。今回は、
トークンへの分解の方法
さて、
これもいくつか考えられますが、
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)で、
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)では、
続いて、
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)では、
この段階のgetchunk()では、
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)では、
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)で、
(8)で、
実は、
繰り返されるinput系の修正
やりかけだった、
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)以降で、
いったんaddcontents()のコードを書きますが、
さて、
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を作ったあと、
その後、
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");
}
}
コメントが取り除かれることを検証するテストを書いたあと、
このあとアセンブラのメインの処理を書いていくのですが、
入力にエラーがあってアセンブル処理を継続できない場合は、
class inputbuffer {
var $linelist;
var $message = null;
/* .... */
function getmessage() {
if (count($this->message) /* (1) */
asserteq(null, $obj->getmessage());
$obj->addmessage("error1");
asserteq("error1", $obj->getmessage());
$obj->addmessage("error2"); /* (2) */
asserteq("error1\nerror2", $obj->getmessage());
}
}
そこで、
アセンブル処理本体に渡るのは、
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)で、
さて、
テスト駆動開発は、設計変更に耐えられるのか
ここまでの流れから、
複雑なものも扱えるのか?
→ 言語の構文解析は複雑。inputまわりを中心に見ていきましたので、
より複雑なものについては、
生産性が本当に上がるのか?
→ (これは未検証)生産性については、
また、
部分でテストしても全体の代わりになるのか?
→ アセンブラとして機能するのかをチェック。inputbufferからinputlineを含んだテストをするなど、
全体のテストが不要になるわけではありませんが、
テスト可能にするとソースが読みにくくなるのでは?
→ 前回のソースと比較。テストできるようにしたため、
たとえば、
また、
ですので、
最初に構造をしっかり設計しないと、あとから変更できないのでは?
→ 何度も大変更を行った。これが本題です。今回は、
先ほどのsetparent()のように、
問題は、
もっと複雑なプログラム
というわけで、
なお、