アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

ユニットテスト、ここどうするの?

こんにちは、斉藤です。

前々回はTDDというユニットテストを使った開発方法前回はユニットテスト実行の自動化と、触れてきました。
今回もやっぱりユニットテストについてということで、方法論などをtips形式でお送りします。





* プライベートメソッドのテスト


ユニットテストでは、公開されているメソッドを呼び出して実行します。
クラスを書いたことがある方なら知っている通り、プライベートメソッドを外部から呼び出すことは出来ません。
そのため一般的にこれは、避けた方が良いと言われていますが・・・自分はテストしたい派なので、以下の方法をご紹介します。

- パブリックメソッドでプライベートメソッドを呼び出す
一番簡単な方法ですね。
ただしこの場合だと、公開APIと非公開APIの区別が出来なくなってしまいます。
クラスに手を入れることになり、テストのためだけのコードが混じってしまうことがデメリットです。

例;


<?php
class Hoge
{
  public function __construct()
  {
  }

  private function doSomething($a, $b)
  {
    return $a + $b;
  }

  // テストでは使うけど、実際には使わない
  public function doSomething2($a, $b)
  {
    return $this->doSomething($a, $b);
  }
}


余談ですが、objective-cでは"クラスに手をつけず、クラスの機能を拡張できる"カテゴリーという機能があります。
この機能を使えば、ユニットテストの時のみ、パブリックメソッドを追加する方法が取れます。

- リフレクションでプライベートメソッドを呼び出す
PHPJAVAなどは、リフレクションという"プログラム自身を読み取る"機能が使えますので、それを使いましょう。
これをテストコードの中のみに記述しておけば、実際に使うときには使われないので安全です。
以下の例では、上記HogeクラスのdoSomethingメソッドをリフレクションで外部から実行しています。



<?php
class HogeTest extends PHPUnit_Framework_TestCase
{
  public function testDoSomething()
  {
    $method = new ReflectionMethod(
      'Hoge', 'doSomething'
    ); 
    $method->setAccessible(true);
    $this->assertEquals(3, $method->invokeArgs(new Hoge(), array(1,2)));
  }
}


参考:
http://sebastian-bergmann.de/archives/881-Testing-Your-Privates.html
http://blog.asial.co.jp/751



* void型メソッドのテスト


値を返さないメソッドをどうやってテストするの?って思いますよね?
割と方法が考えられています。

- プロパティが変化するなら、その値に対してassertをかける
そのメソッド内でプロパティの値が変化するなら、その内容でテストをすることが出来ます。
ただし、このプロパティがパブリックである、もしくはアクセスできるメソッドが公開されている場合に限ります。コメントで、リフレクションを使えば良いということを教えていただきました。



<?php
class Foo
{
  private $a;
  public function __construct($a)
  {
    $this->a = $a;
  }

  public function doSomething()
  {
    $this->a += 5;
  }

  public function getA()
  {
    return $this->a;
  }
}

class FooTest extends PHPUnit_Framework_TestCase
{
  public function testDoSomething()
  {
    $foo = new Foo(5);
    $foo->doSomething();
    $this->assertEquals(10, $foo->getA());
  }
}


- メソッド後にassertを置く
そのメソッドが呼び出されたことだけを確認するならば、テスト失敗時に実行が止まることを利用して、メソッドの後にテストを記述します。
実行結果は分からないですが、とりあえずエラー無く実行できた、ということを判断したい場合にはこれでもテスト可能です。
exceptionを吐く可能性のあるメソッドに対して、有効ですね。

例:


<?php
class Bar
{
  private $a;
  public function __construct()
  {
  }

  public function doSomething($a)
  {
    if ($a!=0) {
      $this->a = 1/$a;
    } else {
      throw new Exception();
    }  
  }
}

class BarTest extends PHPUnit_Framework_TestCase
{
  public function testDoSomething()
  {
    $bar = new Bar();
    $bar->doSomething(0);
    $this->assertEquals(true, true);
  }
}


- その内容を必要な人に伝えるようにする
この方法は目からウロコですね。
全てを自動化する、ということは出来ないという例でもあります。
メソッドの内容によっては、システムが自動的にリカバリーすることは難しいといったケースもあります。
そういう時はメールを飛ばすなりで、運用者が気づくようにする、ということが必要になります。
例えば、"ユーザーが使って買い物をする、WEBシステムで二重課金されてしまった"というパターンでは、運用者が手動でキャンセルをする、ということになります。

参考:
http://stackoverflow.com/questions/246038/unit-testing-void-methods



* テストを走らせることを忘れてしまう、面倒だと感じている


手動で繰り返すと、どうしても気を抜いたときに実行しなくなってしまいます。
JenkinsなどのCIツールや、watchmedoなどの監視ツールを使い、自動的に走らせるようにしましょう。

参考:
http://d.hatena.ne.jp/anatoo/20120402/1333377979
http://www.atmarkit.co.jp/fjava/rensai4/devtool21/devtool21_1.html



* そもそもユニットテストを何のために書くか


もちろんメソッドがきちんと動いていることを確認するためですが・・・自分は以下のように考えています。

- 仕様を反映したテストを書くことで、実装が正しいと認識できる
正常系、異常系を記述し、プログラマー自身が実装を判断するためにも、ユニットテストは有効です。
ただしこれについては、自分はこのようなことを言われています。
「いくらユニットテストを書いても、書いた本人が想像できないことはテストできないよ?」
結局ユニットテストといっても、書き手は実装するエンジニアになるので、その質はエンジニア力が求められます。
ユニットテストが動いているから、実装は正しいんだ」ではなく、「仕様に沿った正常系と異常系をテストする」という考え方をしましょう。

- 自動で実行出来るので、コンピューター任せに出来る
前々回の記事でも書きましたが、shell scriptやCIツールなどを使ってテストを自動実行できます。
手動で実行する手間が省けるだけでも、ユニットテストを頑張って書く甲斐はあります。

- 実装を把握できる
コードの引き継ぎを想定しています。
テストが無ければ、自分が書いたときに正しい、と判断して書いていても他の人が引き継いだ際に、このメソッドが動いているのかどうかが分からない、なんていう事態も起こりうります。
コードの正当性を担保しているユニットテストがあれば、新しい開発者はこれに目を通すことで何が正しいのかをメソッドレベルで確認することが出来ます。
もちろん、引き継ぎ自体もお忘れなく。





* 終わり


前々回、前回、今回とユニットテストについて述べてきましたが、いかがだったでしょうか。
テストを書くことで、自分のコードにより自信を持つことができるようになると思います。
このブログを読んでいただいた方の一人でも多くがユニットテストを書くようになれば、幸いです!

それでは、また。

自動テストするぞ!tmux + PHPUnit + watchmedoで構築する自動ユニットテスト環境

こんにちは、斉藤です。
前回はテスト駆動開発という開発方法をご紹介しました
その中では"ユニットテストの実行"を主体に開発を行っていくことをお伝えしました。
今回は(余計な話も交えつつ)そのユニットテストの実行を自動化する方法をご紹介します。





* なぜ自動化?


ユニットテストを手動で走らせていませんか?
ユニットテストなど、開発プロセスの中で機械がやれることを手動で行うと、そこでスピード(and 効率)がガクッと下がります。機械任せにできるものは、以下のようなタスクがあります。



・ビルド
・ユニットテストの実行
・簡易なドキュメント、チェンジログの用意


手動で行うことにしていると、そのプロセスを実行することを忘れてしまったり、「めんどくさいよー」ってなってしまい、実行回数が減ってしまいます。
その結果、品質の低いアプリが生まれますよね・・・。
近年では、それらを改善するために継続的インテグレーションという習慣が生まれたりしています。
この習慣の中では、自動化できることは自動化しようととも言っていますので、それに倣って自動化できるところはどんどん自動化していきましょう!



* ツールを用意する


まずは、タイトルにあるこの三つのツールを用意します。


・tmux(screenでも可):ターミナルの画面を一度に複数表示できるようにする(場合によっては使わない)
・PHPUnit:ユニットテストを走らせるためのツール(使用している言語によって、変えてください)
・watchmedo:フォルダ内の監視をし、変更が起きたタイミングで、シェルスクリプトを走らせることが出来る


話が長くなりますので、具体的な解説は避けますが、単体で使っても使いやすいツールです。インストール方法はリンク先を参照してください。
tmux
PHPUnit
watchmedo

この三つのツールを組み合わせると、こんなことになります。


・tmuxによって、一つはエディタ、もう一つはユニットテストの結果を表示させる画面を一度に表示できる
・watchmedoによって、エディタでファイルを変更したタイミングで、PHPUnitを実行できる

これで、一々手動でユニットテストを実行させる手間を省くことが出来ます。

* 環境構築


それでは、構築してみます。
まずはターミナルを立ち上げ、tmuxを実行します。



次に、画面を複数表示させます。
tmuxでは、これをペインという方法で実現します(C-b % というキーバインドアサインされています)。
これによって、それぞれの画面で別の作業をし、それを一度に確認できるようになりました!



テストをしたいPHPと、そのユニットテストPHPUnitで書かれたもの)を用意します。
今回はサンプルとして以下のものを用意しています。



テストをしたいPHP(SampleObject.class.php


<?php
class SampleObject {
  private $foo;

  public function __construct()
  {
    $this->foo = "test string";
  }

  public function getFoo()
  {
    return $this->foo;
  }
}
?>


ユニットテスト(sampleTest.php


<?php
require_once("/home/katsuya/auto_unit_test_blog/SampleObject.class.php");

class SampleTest extends PHPUnit_Framework_TestCase {
  public function testPushAndPop()
  {
    $object = new SampleObject();
    $this->assertEquals("test string", $object->getFoo());
  }
}
?>


そして、片方の画面で以下のようなスクリプトを叩きます。



$ watchmedo shell-command --patterns="*.php" -c "clear; phpunit --colors ./"


これで、対象のファイル(ここではカレントディレクトリの中のPHPファイル)が変更されるたびに、ユニットテストすなわち"phpunit --colors ./"というコマンドが実行されます。
あとは、もう片方の画面でエディタを開いて開発をするだけですね!
保存するたびに、自動でユニットテストが走るので、効率よく開発を続けられます。



"自分の書いたコードを保存するというタイミングでフィードバックが得られる"ようになるので、開発効率が改善されるんじゃないかと思います。
ただし、それぞれのツールを使うための準備や知識が必要になりますので、状況に応じて、手動ユニットテストと使い分けてください。



* 備考


以上、自分では便利だ!と思っている環境をご紹介しましたが、最後にいくつか補足します。

- 継続的インテグレーションについて
冒頭でも触れましたが、アンテナを張っている方なら一度は聞いたことがあるんじゃないな?と思います。
アプリケーションを作っていく中で発生する作業の効率を良くするための習慣、と言われています。
本文で触れたように、ビルド、ユニットテスト、などなど機械に自動的にやらせた方が良いものはそうしよう、という手法もこの中の一つの習慣に入ります。
"継続的"という言葉は、一度きりではなく負担無く何度も続けられるようにする、という意味合いだと僕は思っています。
蛇足ですが、弊社はMonacaというサービスを"継続的"に運営、開発をしていますので、社内でもこういった習慣の取り入れを行い、少しでも開発体制を良くしようとしています。

- vimではバックアップファイルが作られること
自分はPHPの開発の際には、vimと呼ばれるエディタを使っています。
が、このエディタを今回の方法で使うと、vimが保存する度にバックアップを作る仕組み上、一度の保存に対してphpunitが複数回走ってしまいます。
今回は以下の設定を施して回避をしています(それでも、二回走ってしまう時があります・・・確実に避けられる方法を知っている方はぜひ。。。)。


・.vimrcファイルにset backupskip=/home/katsuya/auto_unit_test_blog/*  
・watchmedoに起動オプションとして、--patterns="*.php"を指定


- ユニットテストを保存するたびに走らせることの是非
以前、自分が今回紹介した環境を作りたい!と、周りの人に相談したところ、こんなことを言われました。
「保存するたびに、ユニットテストが走るのはそれはそれでうざくない?」
確かに。
軽量なプログラムならともかく、大規模になっていくとテストしていくコストも大きくなるので、そういった場合にはこの環境も見直しが必要になるでしょう。
そういった場合は、テストする対象を絞ったり、JenkinsなどのCIツールでユニットテストを走らせるなどの工夫が必要になると思います。

- PHP以外の他言語でも可能なこと
この記事は弊社のPHPプロというサイトに参照されることになりますので、PHP向けに書きました。
が、PHPUnitの部分を他のツールに読み替えると、JavaScriptQUnit)やobjective-c(GHUnit)にだって対応できます!
まずは、使っている言語のユニットテストフレームワークを探してみてください!





* 終わり


今回は自動でユニットテストを実行させる環境を構築する方法をご紹介しました。
皆さんも手動テストと使い分けて、良いアプリ開発を!

実践TDD! テスト駆動開発入門

こんにちは、斉藤です。
前回のブログをさぼっていたので、あっというまに次のブログの日が来てしまいました。

最近、テスト駆動開発入門(ケントベック著)という本を読んでみて、これは!と思ったので、この開発方法の実践をしてみたいと思います。
今回はQUnitというJavaScriptユニットテストフレームワークを使った方法でのご紹介です。
http://qunitjs.com/





テスト駆動開発(TDD)とは?


ユニットテストを常に書きながら、プログラムを開発していくスタイルのことです。
ユニットテストを先に書くので、プログラムはそれが通るように開発することが求められます。

具体的な開発のサイクル:


1. テストを作成する(表現したいことを確認するテストを作る。)
2. テストをパスする(1で作ったテストをパスする実装を行う。仮実装でも構わない。)
3. リファクタリングを行う(テストを増やし、正しい実装に変更する。)


メリット:


開発終了時には、ユニットテストレベルでの品質保証ができている
常にユニットテストを通すことを目標とするので、メソッド単位でのデグレが起きにくい


デメリット:


ユニットテストを書きながらの開発を行うため、その分の時間がかかる


となっています。実際にどんなことをやるかは後ほど触れていきます。
それでは、始めていきましょう!



QUnit導入



まずはQUnitを使うべく、以下のHTMLとJSを用意しました。

index.html


<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>QUnit Example</title>
  <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.10.0.css">
</head>
<body>
  <div id="qunit"></div>
  <script src="http://code.jquery.com/qunit/qunit-1.10.0.js"></script>
  <script src="tests.js"></script>
</body>
</html>


tests.js


test( "test hello", function() {
  QUnit.equal(1, 1, "1=1");
});


また環境は以下の通りです。


OS: Mac OS X 10.7.5
ブラウザ: Chrome 24.0


これで、ユニットテストを使う環境が揃いました!
ユニットテストや、QUnitについての詳しい解説は他に譲るとして、これをベースにテスト駆動開発を行っていきます。



* 何を作る?


開発するテーマが無いと困るので、冒頭の書籍に倣って、以下の通貨を取り扱うためのプログラムを書いていきたいと思います。


お金を表すオブジェクト
乗法を表現するメソッド
お金同士を比較する方法
異なる通貨の表現


* お金を表すオブジェクト


テスト駆動開発では、先にテストを用意します。
そのため、まずお金を表すオブジェクトのためのテストを用意します。

tests.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money();
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
});

お金を表すため、オブジェクトMoneyとその数量amountを作ろうと思い、それを確認するテストを用意しました。
しかしこのままでは、エラーが表示されてしまいます。
これをパスするために、実際にそのクラスを作ります。

money.js


function Money(){
  this.amount = 1;
};

JSでクラスを表す方法は色々ありますが、今回はシンプルにこの形で書いていくことにしました。テストをパスするために、仮実装としてamountプロパティには1を入れてあります。

index.htmlもこのクラスを読み込むように、scriptタグを追加します。

index.html


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>QUnit</title>
    <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.10.0.css">
  </head>
  <body>
  <div id="qunit"></div>
  <script src="http://code.jquery.com/qunit/qunit-1.10.0.js"></script>
  <script src="money.js"></script>
  <script src="tests.js"></script>
  </body>
</html>


とりあえず、書いたテストはパスします。
しかし、これならどうでしょうか。

tests.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money();
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money();
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

二つ目のテストで落ちますね。
amountプロパティに定数を入れているので当然ですが。。。
これはオブジェクトを生成する際に、amountプロパティに代入するように出来れば良いですね。

test.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});


これに対応して、money.jsを変更します。
money.js


function Money(amount){
  this.amount = amount;
};

これで、1円や5円を表すことが出来ます!

* 乗法を表現するメソッド


次にお金が複数あることを表現するテストを用意したいと思います。

tests.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1);
  oneMoney.times();
  QUnit.equal(oneMoney.amount, 5, "Get 5 money");
});

testTimesというテストを用意しました。
これはまだtimesメソッドが無いのでパスしません。
新しく、そのメソッドを用意します。

money.js


function Money(amount){
  this.amount = amount;

  this.times = function() {
    this.amount *= 5;  
  }
};


これでパスするようにはなりましたが、仮実装のため、"5円"を作ることしか出来ません。
これをリファクタリングしましょう。

test.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1);
  oneMoney.times(5);
  QUnit.equal(oneMoney.amount, 5, "Get 5 money");
});

tests.jsに"timesメソッドは引数が入る"ように修正しました。
money.jsにこれを実装します。

money.js


function Money(amount){
  this.amount = amount;

  this.times = function(multiplier) {
    this.amount *= multiplier; 
  }
};

これでまたパスしました!テストケースを増やしましょう。



test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1);
  oneMoney.times(5);
  QUnit.equal(oneMoney.amount, 5, "Get 5 money");
  oneMoney.times(10);
  QUnit.equal(oneMoney.amount, 10, "Get 10 money");
});


パスしなくなりました。
1円を表すoneMoneyがtimesメソッドを複数回行うと、何か変になることに気づきました。
よくよく考えてみると、timesメソッドを行うと、oneMoneyという名前ではおかしいですね。
"1円"を表すオブジェクトoneMoneyがtimesメソッド後、5円を表す、というバグに気づきました(amount値が変わるが、変数名がそのまま)。

これを修正したいので、今回はtimesメソッドは新しくMoneyオブジェクトを返すこととしましょう。

tests.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1);
  var fiveMoney = oneMoney.times(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  var tenMoney = oneMoney.times(10);
  QUnit.equal(tenMoney.amount, 10, "Get 10 money");
});


これをパスするよう、timesメソッド実行時にはamount値は書き換えず、新しくMoneyオブジェクトを返す実装を施しました。

money.js


function Money(amount){
  this.amount = amount;

  this.times = function(multiplier) {
    return new Money(this.amount * multiplier);
  }
};


これで、timesメソッドの実装、リファクタリングを終えました。
ユニットテストも残っているので、きちんと動作していることに自信が持てますね!

* お金同士を比較する方法


お金同士を比較するとどうなるでしょうか。
設計としては5円==5円はtrueが返ってきて欲しいと考えています。

tests.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1);
  var fiveMoney = oneMoney.times(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  var tenMoney = oneMoney.times(10);
  QUnit.equal(tenMoney.amount, 10, "Get 10 money");
});

test( "testEquals", function() {
  QUnit.equal(new Money(5), new Money(5), "Test money equality");
});

tests.jsにtestEqualsというテストを用意しました。
5円を表すnew Money(5)は別のnew Money(5)と同じでしょうか。
実行すると、エラーになり、同じではないと返ってきました。
JSコードレベルでのイコールを使用するのではなく、設計レベルでのイコールが必要なようです。
そういった機能を持つメソッドを使うようなテストに書き換えます。

tests.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1);
  var fiveMoney = oneMoney.times(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  var tenMoney = oneMoney.times(10);
  QUnit.equal(tenMoney.amount, 10, "Get 10 money");
});

test( "testEquals", function() {
  QUnit.equal(new Money(5).equals(new Money(5)), true, "Test money equality");
});

もちろん、このままではequalsメソッドが実装されておらずパスしないので、パスするよう実装を行います。

money.js


function Money(amount){
  this.amount = amount;

  this.times = function(multiplier) {
    return new Money(this.amount * multiplier);
  }
  this.equals = function(money) {
    return true;
  }
};

仮実装により、テストが通ることを確認しました。
仮実装を行う必要が無く、すぐに正しい実装を行えるのならば、それで構いません。
今回は紹介のため、仮実装を行うという手順を踏んでいます。
この仮実装が正しいかどうかをテストを増やすことで確認しましょう。

tests.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1);
  var fiveMoney = oneMoney.times(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  var tenMoney = oneMoney.times(10);
  QUnit.equal(tenMoney.amount, 10, "Get 10 money");
});

test( "testEquals", function() {
  QUnit.equal(new Money(5).equals(new Money(5)), true, "Test money equality");
  QUnit.equal(new Money(5).equals(new Money(8)), false, "Test money equality");
});

当然エラーになりますね。
これをパスするよう、正しい実装にしましょう。
お金同士が同値であることを設計するには、Moneyオブジェクトのamount値を比較すれば大丈夫です。

money.js


function Money(amount){
  this.amount = amount;

  this.times = function(multiplier) {
    return new Money(this.amount * multiplier);
  }
  this.equals = function(money) {
    return this.amount == money.amount;
  }
};

これで、テストをパスするようになりました!
equalsメソッドはnullやMoney以外のオブジェクトがパラメーターとして渡された場合どうなるかが考えられますが、現在は必要ではないとして、割愛します。

今実装したequalsメソッドを使って、テストをスリムにします。
tests.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.times(5).equals(new Money(5)), true, "Get 5 money");
  QUnit.equal(oneMoney.times(10).equals(new Money(10)), true, "Get 10 money");
});

test( "testEquals", function() {
  QUnit.equal(new Money(5).equals(new Money(5)), true, "Test money equality");
  QUnit.equal(new Money(5).equals(new Money(8)), false, "Test money equality");
});


* 異なる通貨の表現


ここまでで、お金のオブジェクトは単位が円であることを暗黙的に用いてきましたが、そろそろお金を通貨として、異なる通貨単位を取り扱ってみます。
まず、テストを作成します。

money.js


test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5);
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1);
  QUnit.equal(oneMoney.times(5).equals(new Money(5)), true, "Get 5 money");
  QUnit.equal(oneMoney.times(10).equals(new Money(10)), true, "Get 10 money");
});

test( "testEquals", function() {
  QUnit.equal(new Money(5).equals(new Money(5)), true, "Test money equality");
  QUnit.equal(new Money(5).equals(new Money(8)), false, "Test money equality");
});

test( "testCurrencyEquality", function() {
  var oneYen = new Money(1);
  var oneDoller = new Money(1);
  QUnit.equal(oneYen.equals(oneDoller), false, "test");
});


テストはパスしません。
equalsメソッドを用いても、oneYenとoneDolloerが同値であると言われてしまいます。
まだMoneyオブジェクトは通貨単位を持っておらず、amount値のみの比較なので当然ですね。
テストとして、通貨単位を扱うプロパティを持つようテストを修正します。



test( "testGetMoneyObject", function() {
  var oneMoney = new Money(1, "YEN");
  QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  var fiveMoney = new Money(5, "YEN");
  QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
});

test( "testTimes", function() {
  var oneMoney = new Money(1, "YEN");
  QUnit.equal(oneMoney.times(5).equals(new Money(5, "YEN")), true, "Get 5 money");
  QUnit.equal(oneMoney.times(10).equals(new Money(10, "YEN")), true, "Get 10 money");
});
 
test( "testEquals", function() {
  QUnit.equal(new Money(5, "YEN").equals(new Money(5, "YEN")), true, "Test money equality");
  QUnit.equal(new Money(5, "YEN").equals(new Money(8, "YEN")), false, "Test money equality");
});

test( "testCurrencyEquality", function() {
  var oneYen = new Money(1, "YEN");
  var oneDoller = new Money(1, "USD");
  QUnit.equal(oneYen.equals(oneDoller), false, "test");
});


これをパスするよう、Moneyオブジェクトにcurrencyという引数を追加します。
money.js


function Money(amount, currency) {
  this.amount = amount;
  this.currency = currency;

  this.times = function(multiplier) {
    return new Money(this.amount * multiplier, currency);
  }
  this.equals = function(money) {
    return this.amount == money.amount;
  }
};


まだパスしません。
問題のequalsメソッドを確認しましょう。
設計レベルで異なる通貨はイコールでないとしました。
これを実装します。

money.js


function Money(amount, currency) {
  this.amount = amount;
  this.currency = currency;

  this.times = function(multiplier) {
    return new Money(this.amount * multiplier, this.currency);
  }
  this.equals = function(money) {
    return this.amount == money.amount  & & this.currency == money.currency;
  }
};

数量同士の比較に加え、通貨同士も比較するようにし、equalsメソッドが正しく動くようになりました。
これで、最初に挙げた実装したい四つが実装できたことになります。

この書籍ではまだまだ続きますが、ブログではここで一旦閉めます。





* 終わりに


今回はテスト駆動開発とは何か?ということを実践を交えながら、説明しました。
この手法だと、ユニットテストが蓄積されていくことが嬉しいですね!
同じテストを手動で繰り返すことが無くなるので、設計や違うことに時間をかけることが出来ます。
やはりテストは出来るだけコンピューターに任せることが一番ですね。
皆さんもぜひやってみてください!

PHPSpecでユニットテスト

こんにちは、牧野です。
今回は、PHPユニットテストを行う際に便利なユニットテストフレームワーク、PHPSpecの紹介です。
PHPSpecは、phpで振舞駆動開発ができるようにと作られたものです。
PHPSpecの詳細はこちら
http://dev.phpspec.org/manual/ja/
日本語マニュアルが大変充実しています。

振舞駆動開発について簡単に説明すると、テストありきの開発手法であるテスト駆動開発を発展させたような開発手法で、このプログラムはこんな動作をするべきだ、という要求仕様(スペック)を、そのままテストコードとして記述しながら開発を進めていく、という感じのものです。PHPSpecのような振舞駆動開発用フレームワークでは、要求仕様がすぐわかるようなテストコードの書き方をするようになっているので開発効率が上がるようです。

ではさっそく、まずはインストールについてですが、pearコマンドを使うとすごく簡単にインストールできます。
ルートユーザで


pear channel-discover pear.phpspec.org
pear install phpspec/PHPSpec

を実行すればOKです。

使い方、というか振舞駆動開発の流れですが、
1.要求仕様を文章に直し(it should ... の形式で)、それに沿ったテストコード(スペックファイル)を作成。
2.作ったスペックファイルで正しい結果が出てくるようなプログラムを作成。
3.スペックファイルを完成させてテストを実行して、正しい結果になることを確認。

1から3の繰り返しになります。
マニュアルにわかりやすい例がいろいろ載っているので、ぜひぜひそちらを見てみて下さい。

一応、やってみると、、、
MyStringクラスという、渡した文字列をどんどんつなげていくクラスを作ることにします。
まずはスペックファイルの作成です。

DescribeMyString.php


<?php
class DescribeMyString extends PHPSpec_Context {
  public function itShouldBeEmptyIfNoArgs() {
    $str = new MyString();
    $this->spec($str->body)->should->beNull();
  }

  public function itShouldBeCorrectString() {
    $str = new MyString('aaa');
    $str->add('bbb');
    $this->spec($str->body)->should->beEqualTo('aaabbb');
  }
}
?>


とりあえずこんな感じで要求仕様を表すスペックファイルを作ります。
ファイル名はDescribe~.phpに、クラス名はDescribe~にして、PHPSpec_Contextを継承させます。
メソッド名はit should ... の形式にします。これで、メソッド名を見ただけで何となく要求仕様がわかるようになります。
というか、要求仕様がわかるようなメソッド名をつけるようにします。…2つ目のメソッド名はあまりよくないかもしれません。

次に、実際にMyStringクラスを作ります。

MyString.php


<?php
class MyString {
  public $body;

  function MyString($string = null) {
    $this->body = $string;
  }

  function add($string) {
    $this->body .= $string;
  }
}
?>


クラスを実装したら、スペックファイルを少し直して、

DescribeMyString.php


<?php
require_once('MyString.php');

class DescribeMyString extends PHPSpec_Context {

  public function itShouldBeEmptyIfNoArgs() {
    $str = new MyString();
    $this->spec($str->body)->should->beNull();
  }

  public function itShouldBeCorrectString() {
    $str = new MyString('aaa');
    $str->add('bbb');
    $this->spec($str->body)->should->beEqualTo('aaabbb');
  }
}
?>


テストしてみます。


phpspec DescribeMyString

スペックファイルが複数ある場合は


phpspec -r

でまとめてテストできます。
-sをつけると、テスト結果が文章の形式で出力されます。メソッド名をちゃんとつけていないと、変な文章になります。。
うまくいくと、次のような出力結果が出ます。

あとは、さらに要求仕様を詰めてスペックファイルに追加、クラスの実装、テストを繰り返すことになります。


実際に初めて使ってみた率直な感想ですが、、、かなり面倒に感じました。。スペックファイルを作っている時に、何当たり前な事書いてるんだろう、という気がしてきます。
クラスの具体的な振舞はいくらでも考えられるので、細かく書こうとするとどこまでも細かく書けてしまって大変なことになります。

少しいいかも、と思ったのは、クラス内の実装を変更した時でした。phpspecコマンドを実行するだけで、とりあえず正しく変更できているかどうかがわかります。
慣れの問題もあると思うので、システムの規模が大きくなって多くのクラスを作らなければいけないような時は、けっこう使えるかもしれません。