Asial Blog

Recruit! Asialで一緒に働きませんか?

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

カテゴリ :
フロントエンド(HTML5)
タグ :
Tech
JavaScript
ユニットテスト
こんにちは、斉藤です。
前回のブログをさぼっていたので、あっというまに次のブログの日が来てしまいました。

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





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


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

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

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

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

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



* QUnit導入



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

index.html
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.   <meta charset="utf-8">
  5.   <title>QUnit Example</title>
  6.   <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.10.0.css">
  7. </head>
  8. <body>
  9.   <div id="qunit"></div>
  10.   <script src="http://code.jquery.com/qunit/qunit-1.10.0.js"></script>
  11.   <script src="tests.js"></script>
  12. </body>
  13. </html>

tests.js
  1. test( "test hello", function() {
  2.   QUnit.equal(1, 1, "1=1");
  3. });

また環境は以下の通りです。
  1. OS: Mac OS X 10.7.5
  2. ブラウザ: Chrome 24.0

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



* 何を作る?


開発するテーマが無いと困るので、冒頭の書籍に倣って、以下の通貨を取り扱うためのプログラムを書いていきたいと思います。
  1. お金を表すオブジェクト
  2. 乗法を表現するメソッド
  3. お金同士を比較する方法
  4. 異なる通貨の表現

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


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

tests.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money();
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4. });
お金を表すため、オブジェクトMoneyとその数量amountを作ろうと思い、それを確認するテストを用意しました。
しかしこのままでは、エラーが表示されてしまいます。
これをパスするために、実際にそのクラスを作ります。

money.js
  1. function Money(){
  2.   this.amount = 1;
  3. };
JSでクラスを表す方法は色々ありますが、今回はシンプルにこの形で書いていくことにしました。テストをパスするために、仮実装としてamountプロパティには1を入れてあります。

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

index.html
  1. <!DOCTYPE html>
  2. <html>
  3.   <head>
  4.     <meta charset="utf-8">
  5.     <title>QUnit</title>
  6.     <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.10.0.css">
  7.   </head>
  8.   <body>
  9.   <div id="qunit"></div>
  10.   <script src="http://code.jquery.com/qunit/qunit-1.10.0.js"></script>
  11.   <script src="money.js"></script>
  12.   <script src="tests.js"></script>
  13.   </body>
  14. </html>

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

tests.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money();
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money();
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
二つ目のテストで落ちますね。
amountプロパティに定数を入れているので当然ですが。。。
これはオブジェクトを生成する際に、amountプロパティに代入するように出来れば良いですね。

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

これに対応して、money.jsを変更します。
money.js
  1. function Money(amount){
  2.   this.amount = amount;
  3. };
これで、1円や5円を表すことが出来ます!

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


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

tests.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1);
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5);
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1);
  10.   oneMoney.times();
  11.   QUnit.equal(oneMoney.amount, 5, "Get 5 money");
  12. });
testTimesというテストを用意しました。
これはまだtimesメソッドが無いのでパスしません。
新しく、そのメソッドを用意します。

money.js
  1. function Money(amount){
  2.   this.amount = amount;
  3.  
  4.   this.times = function() {
  5.     this.amount *= 5;  
  6.   }
  7. };

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

test.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1);
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5);
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1);
  10.   oneMoney.times(5);
  11.   QUnit.equal(oneMoney.amount, 5, "Get 5 money");
  12. });
tests.jsに"timesメソッドは引数が入る"ように修正しました。
money.jsにこれを実装します。

money.js
  1. function Money(amount){
  2.   this.amount = amount;
  3.  
  4.   this.times = function(multiplier) {
  5.     this.amount *= multiplier; 
  6.   }
  7. };
これでまたパスしました!テストケースを増やしましょう。

  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1);
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5);
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1);
  10.   oneMoney.times(5);
  11.   QUnit.equal(oneMoney.amount, 5, "Get 5 money");
  12.   oneMoney.times(10);
  13.   QUnit.equal(oneMoney.amount, 10, "Get 10 money");
  14. });

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

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

tests.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1);
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5);
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1);
  10.   var fiveMoney = oneMoney.times(5);
  11.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  12.   var tenMoney = oneMoney.times(10);
  13.   QUnit.equal(tenMoney.amount, 10, "Get 10 money");
  14. });

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

money.js
  1. function Money(amount){
  2.   this.amount = amount;
  3.  
  4.   this.times = function(multiplier) {
  5.     return new Money(this.amount * multiplier);
  6.   }
  7. };

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

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


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

tests.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1);
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5);
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1);
  10.   var fiveMoney = oneMoney.times(5);
  11.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  12.   var tenMoney = oneMoney.times(10);
  13.   QUnit.equal(tenMoney.amount, 10, "Get 10 money");
  14. });
  15.  
  16. test( "testEquals", function() {
  17.   QUnit.equal(new Money(5), new Money(5), "Test money equality");
  18. });
tests.jsにtestEqualsというテストを用意しました。
5円を表すnew Money(5)は別のnew Money(5)と同じでしょうか。
実行すると、エラーになり、同じではないと返ってきました。
JSコードレベルでのイコールを使用するのではなく、設計レベルでのイコールが必要なようです。
そういった機能を持つメソッドを使うようなテストに書き換えます。

tests.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1);
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5);
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1);
  10.   var fiveMoney = oneMoney.times(5);
  11.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  12.   var tenMoney = oneMoney.times(10);
  13.   QUnit.equal(tenMoney.amount, 10, "Get 10 money");
  14. });
  15.  
  16. test( "testEquals", function() {
  17.   QUnit.equal(new Money(5).equals(new Money(5)), true, "Test money equality");
  18. });
もちろん、このままではequalsメソッドが実装されておらずパスしないので、パスするよう実装を行います。

money.js
  1. function Money(amount){
  2.   this.amount = amount;
  3.  
  4.   this.times = function(multiplier) {
  5.     return new Money(this.amount * multiplier);
  6.   }
  7.   this.equals = function(money) {
  8.     return true;
  9.   }
  10. };
仮実装により、テストが通ることを確認しました。
仮実装を行う必要が無く、すぐに正しい実装を行えるのならば、それで構いません。
今回は紹介のため、仮実装を行うという手順を踏んでいます。
この仮実装が正しいかどうかをテストを増やすことで確認しましょう。

tests.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1);
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5);
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1);
  10.   var fiveMoney = oneMoney.times(5);
  11.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  12.   var tenMoney = oneMoney.times(10);
  13.   QUnit.equal(tenMoney.amount, 10, "Get 10 money");
  14. });
  15.  
  16. test( "testEquals", function() {
  17.   QUnit.equal(new Money(5).equals(new Money(5)), true, "Test money equality");
  18.   QUnit.equal(new Money(5).equals(new Money(8)), false, "Test money equality");
  19. });
当然エラーになりますね。
これをパスするよう、正しい実装にしましょう。
お金同士が同値であることを設計するには、Moneyオブジェクトのamount値を比較すれば大丈夫です。

money.js
  1. function Money(amount){
  2.   this.amount = amount;
  3.  
  4.   this.times = function(multiplier) {
  5.     return new Money(this.amount * multiplier);
  6.   }
  7.   this.equals = function(money) {
  8.     return this.amount == money.amount;
  9.   }
  10. };
これで、テストをパスするようになりました!
equalsメソッドはnullやMoney以外のオブジェクトがパラメーターとして渡された場合どうなるかが考えられますが、現在は必要ではないとして、割愛します。

今実装したequalsメソッドを使って、テストをスリムにします。
tests.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1);
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5);
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1);
  10.   QUnit.equal(oneMoney.times(5).equals(new Money(5)), true, "Get 5 money");
  11.   QUnit.equal(oneMoney.times(10).equals(new Money(10)), true, "Get 10 money");
  12. });
  13.  
  14. test( "testEquals", function() {
  15.   QUnit.equal(new Money(5).equals(new Money(5)), true, "Test money equality");
  16.   QUnit.equal(new Money(5).equals(new Money(8)), false, "Test money equality");
  17. });

* 異なる通貨の表現


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

money.js
  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1);
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5);
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1);
  10.   QUnit.equal(oneMoney.times(5).equals(new Money(5)), true, "Get 5 money");
  11.   QUnit.equal(oneMoney.times(10).equals(new Money(10)), true, "Get 10 money");
  12. });
  13.  
  14. test( "testEquals", function() {
  15.   QUnit.equal(new Money(5).equals(new Money(5)), true, "Test money equality");
  16.   QUnit.equal(new Money(5).equals(new Money(8)), false, "Test money equality");
  17. });
  18.  
  19. test( "testCurrencyEquality", function() {
  20.   var oneYen = new Money(1);
  21.   var oneDoller = new Money(1);
  22.   QUnit.equal(oneYen.equals(oneDoller), false, "test");
  23. });

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

  1. test( "testGetMoneyObject", function() {
  2.   var oneMoney = new Money(1, "YEN");
  3.   QUnit.equal(oneMoney.amount, 1, "Get 1 money");
  4.   var fiveMoney = new Money(5, "YEN");
  5.   QUnit.equal(fiveMoney.amount, 5, "Get 5 money");
  6. });
  7.  
  8. test( "testTimes", function() {
  9.   var oneMoney = new Money(1, "YEN");
  10.   QUnit.equal(oneMoney.times(5).equals(new Money(5, "YEN")), true, "Get 5 money");
  11.   QUnit.equal(oneMoney.times(10).equals(new Money(10, "YEN")), true, "Get 10 money");
  12. });
  13.  
  14. test( "testEquals", function() {
  15.   QUnit.equal(new Money(5, "YEN").equals(new Money(5, "YEN")), true, "Test money equality");
  16.   QUnit.equal(new Money(5, "YEN").equals(new Money(8, "YEN")), false, "Test money equality");
  17. });
  18.  
  19. test( "testCurrencyEquality", function() {
  20.   var oneYen = new Money(1, "YEN");
  21.   var oneDoller = new Money(1, "USD");
  22.   QUnit.equal(oneYen.equals(oneDoller), false, "test");
  23. });

これをパスするよう、Moneyオブジェクトにcurrencyという引数を追加します。
money.js
  1. function Money(amount, currency) {
  2.   this.amount = amount;
  3.   this.currency = currency;
  4.  
  5.   this.times = function(multiplier) {
  6.     return new Money(this.amount * multiplier, currency);
  7.   }
  8.   this.equals = function(money) {
  9.     return this.amount == money.amount;
  10.   }
  11. };

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

money.js
  1. function Money(amount, currency) {
  2.   this.amount = amount;
  3.   this.currency = currency;
  4.  
  5.   this.times = function(multiplier) {
  6.     return new Money(this.amount * multiplier, this.currency);
  7.   }
  8.   this.equals = function(money) {
  9.     return this.amount == money.amount && this.currency == money.currency;
  10.   }
  11. };
数量同士の比較に加え、通貨同士も比較するようにし、equalsメソッドが正しく動くようになりました。
これで、最初に挙げた実装したい四つが実装できたことになります。

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





* 終わりに


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