TDD Advent Calendar 2013の参加状況確認・参加登録 – Qiita [キータ]
http://qiita.com/advent-calendar/2013/tddadventjp/participants
の一発目です。トップバッターなので、ノーマルに MSTest を使う話を1時間ほど書いていきます。TDDで何ができるの?とか、PHPUnitとか、NUnitとか、gtestとか、CppUnitとか、その他もろもろのお話は先行き出てくるはず(?)なので、そちらに譲るとして、Visual Studio 2013 の Express 2013 for Widnwos Desktop を使って、MSTest を使って見よう、という感じで進めていきます。
■いきなりUIを作る
こんな感じで、いきなりUIを作ってみます。最初のTextBox1とTextBox2の内容を足したら、TextBox3に入る、という簡単な計算機ですね。つーか、簡単すぎてあれなのですが、まあ、これを作っていきます。
■いきなり計算をする。
1 2 3 4 5 6 7 8 | /// 計算する private void button1_Click( object sender, EventArgs e) { int x = int .Parse(textBox1.Text); int y = int .Parse(textBox2.Text); int z = x + y; textBox3.Text = z.ToString(); } |
いきなりコードを書いていきます。これぐらいのコードならばUIにそのまま書いていいよね、って具合に書いていきますが、これじゃダメなのはわかりきってますね。だって、TextBox1.Text が空欄のときは、int.Parse でエラーになるし、なんか、いろいろ TDD 的にダメなところが満載です。
デバッグ実行して、ちまちまとエラー箇所を直すのもアリなのですが、ひと手間かけて、もう少しなんとかしましょう。
■ボタンクリックと計算する部分を分ける
せめて「UIとロジック」を分けようということで、ボタンをクリックしたところと、実際に計算するところを分けます。テキストボックスの文字列をそのまま渡して、文字列でもらってくると関数として楽ですよね、という発想です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /// 計算するボタンを押す private void button1_Click( object sender, EventArgs e) { textBox3.Text = calc(textBox1.Text, textBox2.Text); } /// 計算する private string calc( string x, string y) { int xx = 0; int yy = 0; if ( int .TryParse(x, out xx) == false ) return "" ; if ( int .TryParse(y, out yy) == false ) return "" ; int zz = xx + yy; return zz.ToString(); } |
まあ、これも悪くはないのですが…、ええと、MSTest を使うときは、private メソッドでは無理なので、public にしないとダメですね。
■テスト対象を public にする
calc メソッドを public にしてテストから呼び出せるようにします。
1 2 3 4 5 6 7 8 9 10 11 | /// 計算する public string calc( string x, string y) { int xx = 0; int yy = 0; if ( int .TryParse(x, out xx) == false ) return "" ; if ( int .TryParse(y, out yy) == false ) return "" ; int zz = xx + yy; return zz.ToString(); } |
おお、これなら calc メソッドがテストできそうですね。
■テストコードを書く
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using WindowsFormsApplication1; namespace UnitTestProject1 { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { var frm = new Form1(); string ans = frm.calc( "1" , "2" ); Assert.AreEqual( "3" , ans); } } } |
こんな感じで、文字列の「1」と文字列の「2」を渡したら文字列の「3」を戻すコードを書きます。参照設定で、フォームのあるプロジェクトを設定するのと、System.Windows.Forms も参照設定するのを忘れずに。ちょっとややこしいですね。
無事、テストが成功するとこんな風に緑のチェックマークが出ます。
そうそう、テキストボックスが空欄の場合も追加して実行すると、
こんな風に次々とテストが書けます。めでたしめでたし…で終わると怒られそうですね。フォームをそのままnewするのは、まあそこそこ業務のテストとしてはあるのですが、しかしフォームにいろいろな部品が乗っかったり、フォームを初期化するときにデータベースに接続したりするときは、テストが重くなりそうです。まあ、実際重くなるし、初期化で失敗したりしてテストが自動化できなくなってしまいます。
■フォームとは別のクラスにする
フォームをnewしたくないので、calcメソッドを別のクラスに移します。別にLogicクラスというのを作って、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Logic { /// 計算する public string calc( string x, string y) { int xx = 0; int yy = 0; if ( int .TryParse(x, out xx) == false ) return "" ; if ( int .TryParse(y, out yy) == false ) return "" ; int zz = xx + yy; return zz.ToString(); } } |
フォームのボタンクリック部分を書き換えます。
1 2 3 4 5 6 | /// 計算するボタンを押す private void button1_Click( object sender, EventArgs e) { var l = new Logic(); textBox3.Text = l.calc(textBox1.Text, textBox2.Text); } |
そうして、テストも書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | namespace UnitTestProject1 { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { var l = new Logic(); string ans = l.calc( "1" , "2" ); Assert.AreEqual( "3" , ans); } [TestMethod] public void Test空欄の場合() { var l = new Logic(); string ans = l.calc( "" , "2" ); Assert.AreEqual( "" , ans); } } } |
フォーム自体を呼び出さなくなったので、System.Windows.Forms の参照設定はいらなくなります。テスト対象のアセンブリ(この場合は、Form1を含むアセンブリ)を参照設定します。
ほどよく、いい感じになってきました。
■ロジックをシンプルにする
さて、calc メソッドは、足し算をするメソッドですが、引数が文字列になっています。文字列の足し算は文字列の連結に見えるし、数値だけを足すようにしたほうがロジックがシンプルになりますよね。なので、calc メソッドの引数を数値に変えましょう。
1 2 3 4 5 | public int calc( int x, int y) { int z = x + y; return z; } |
UI のほうは、数値を渡すように変更をします。
1 2 3 4 5 6 7 8 9 10 11 12 | private void button1_Click( object sender, EventArgs e) { string x = textBox1.Text; string y = textBox2.Text; int xx, yy; if ( int .TryParse(x, out xx) == false ) return ; if ( int .TryParse(y, out yy) == false ) return ; var l = new Logic(); int ans = l.calc(xx, yy); textBox3.Text = ans.ToString(); } |
次いでにテストのほうも書き換えます。
1 2 3 4 5 6 | public void TestMethod1() { var l = new Logic(); int ans = l.calc(1,2); Assert.AreEqual(3, ans); } |
空欄の場合は、UIのところでチェックをしているので、テストを取り除きます。
calc メソッドは、必ず数値が渡されるので、ロジックがシンプルになっています。その分、UIで数値以外をチェックしているので、UIが複雑になっていますが。いやいや、これはこれで便利なのです。
■UIで空欄と数値以外でメッセージを表示する
UIの入力時に、空欄のときと、数値以外の2つのパターンでエラーメッセージを表示してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | private void button1_Click( object sender, EventArgs e) { string x = textBox1.Text; string y = textBox2.Text; if (x == "" ) { MessageBox.Show( "xが空欄です" ); return ; } if (y == "" ) { MessageBox.Show( "yが空欄です" ); return ; } int xx, yy; if ( int .TryParse(x, out xx) == false ) { MessageBox.Show( "xに数値を入れてください" ); return ; } if ( int .TryParse(y, out yy) == false ) { MessageBox.Show( "yに数値を入れてください" ); return ; } var l = new Logic(); int ans = l.calc(xx, yy); textBox3.Text = ans.ToString(); } |
結構長くなりますが、テキストボックスに入力した値をひとつひとつチェックすることができますね(この部分をテスト可能にすることもできるのですが、ここではフォームを手動でテストするということで)。ロジックの calc メソッドでは「計算する」ことだけに集中できます。
■答えが10以上の時に、エラーにする
今度は、計算結果が10以上になったらエラーにする処理を追加してみましょう。10以上になることは、calc メソッド内でしかわからないので、例外処理を入れます。
1 2 3 4 5 6 7 8 9 | public int calc( int x, int y) { int z = x + y; if ( z >= 10 ) { throw new Exception( "答えが10以上です" ); } return z; } |
UI のほうでは、calc メソッドを呼び出したときに例外が発生したらメッセージを表示させます。
1 2 3 4 5 6 7 8 9 10 | try { var l = new Logic(); int ans = l.calc(xx, yy); textBox3.Text = ans.ToString(); } catch ( Exception ex) { MessageBox.Show(ex.Message); } |
■例外をチェックするテストコードを書く
どうせなので、テストコードも追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | [TestMethod] public void Test例外発生() { var l = new Logic(); try { int ans = l.calc(5, 6); } catch { // 例外が発生すればOK return ; } // 例外が発生ない場合 Assert.Fail(); } |
例外が発生しない場合に「失敗」になります。
■テストコードから書いてみる
今度は、テストコードのほうを先に書いてみましょう。calcメソッドの引数が0未満の場合はエラーにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public void Test引数チェック1() { var l = new Logic(); try { int ans = l.calc(-1, 6); } catch { // 例外が発生すればOK return ; } // 例外が発生ない場合 Assert.Fail(); } public void Test引数チェック2() { var l = new Logic(); try { int ans = l.calc(0, -1); } catch { // 例外が発生すればOK return ; } // 例外が発生ない場合 Assert.Fail(); } |
引数は2つあるので、テストは2つ必要です。これを実行すると2つ「失敗」します。
■テストが通るようにロジックを書き直す
ロジックのほうを直します。テストをしたときにオールグリーンになればOKです。
1 2 3 4 5 6 7 8 9 10 11 12 13 | public int calc( int x, int y) { if (x < 0 || y < 0) { throw new Exception( "数値は0以上を指定してください" ); } int z = x + y; if ( z >= 10 ) { throw new Exception( "答えが10以上です" ); } return z; } |
■なぜ、ロジックをUIから分離させるのか?
ここまで試した方はわかると思いますが(いやいや、TDDだから周知の事実かもしれないけど)、分離したほうがテストがしやすいのです。逆にいえば、UIべったりにロジックを入れてしまうとテストしずらい。あるいは、テストできないコードになります。まあ、UI自体をテストすることも可能なので、テストできない訳ではないのですが。それはそれとして、TDDの基本の基本はここからスタートです。ケントベックの「テスト駆動開発入門」は残念ながら絶版扱いになっていますが、このチュートリアル(Phythonで書かれている) を1回通してみると、どこにTDDを使えば「効果的なのか」がわかります。逆に言えば、作業量を考えたときに効果的でないところにTDDを利用してもダメなんです、ということです。
で、私がプログラムを書くときに気を付けるのは、テストがしやすいクラスあるいは設計をする、ところに注力してます。テスト可能なクラス≒テストしやすい設計≒不具合対処がしやすい、というパターンですね。まあ、うまくいかないことも多々あるので、完全にという訳にはいきませんが、コアな部分(ライブラリ部分)をテスト可能にしておくのは必須かな、と常々思っています。まあ、ひとまず、1日目のチュートリアルということで。