コントローラのテストを書く

次に、

どんなテストが書けるとうれしいのだろうか?

最初に思いつくのは、サーバーとブラウザのインタラクションの部分、TurboGearsであればコントローラ(controllers.py)とテンプレート(.kid)のつなぎの部分だ。コントローラが出力したデータが、正しくテンプレートないしブラウザ上のJavaScriptで処理できること。また、フォーム(ということはテンプレート)ないしはJavaScript(AJAXですね)から、適切なデータをコントローラが受け取れること。結合テストと呼んでもいい。

本来の意味では結合テストをするには、サーバーとクライアントをそれぞれ通信させて動かしてやらなくてはならない。それはそれで必要なんだけど、すべてを手動+目検でやるのはしんどいし回帰性がない。できるだけテストを書いて対応しようと思うと、サーバー側のテストは以下のように書ける。

  • コントローラにこんな値(A)を渡すと、こんな出力(B)が得られる。という、AとBの対応をテストで書ける。

レベルとしては結合テストレベルの、サーバー上のロジックを確認するためのテストを書く。見方を変えると、コントローラが外部から見てどんな動きをするのか、テストで仕様を表現するということにもなる。

コントローラの入力はブラウザからのリクエスト(FORMまたはJavaScriptからの呼び出し)だし、コントローラの出力はテンプレートまたはJavaScriptへの出力だ。コントローラの入出力インターフェース仕様をテストで定めてやれば、テンプレートも書きやすくなるわけだ。

このテストを書くときのネックは、SQLObjectのせいなのだが、DB上にデータが必要なことである。まあ結合テストレベルって言ってるんだから当然なんだけど、技術的にもモデルの部分をモックに置き換えるのは難しいと思う(なんか方法あるのかな?)

TurboGears.testutilsには、DBTestという、DB用のテストケースベースクラスがある(unittest.TestCaseを継承している)。これはなにをしてくれるかと言うと、

  1. オンメモリのsqliteを設定する
  2. モデルを読み込んで、(オンメモリのsqliteに)テーブルを新規作成する
  3. tearDownですべてロールバックする(ただしこれは、コントローラからのテストでは機能しない。コミットされてしまう)

というわけで、DB上に正しいテストデータを前もって用意しておく、という心配はなくなるわけだ。そのかわりにもちろん、テストケースの中でテストデータを作ってやらないといけない。これはけっこう面倒くさいのだが、とりあえずはディスク上のDBにデータを用意したりそれを復元したりするのよりはマシだと思う。

(ここは人によって意見が分かれるところだと思う。一定のテストデータを作っておいて、それで初期化したDBに対して一連のテストを流す、というのもいい方法だ。ただ、たくさんのテストに対して一貫したテストデータを作るという自信がないので、自分はあまりやりたくない。)

テストケースは、以下のような枠組みになる。

  1. テスト用のデータを作成する。
  2. testutil.call(cherrypy.root.recent)で、コントローラを実行する。
  3. 戻り値を、期待値と比較する。

DBTestはtearDown()ですべてのDBアクセスをロールバックしてくれるのだが、コントローラがどこかでコミットをかけてしまっているため、テスト後にデータが残ってしまう。(どこかは突き止めていない。なんとかなるかもしれない。dburiでautoCommitをon/offしてもダメなのは確認済み) 仕方がないので、自分のtearDown()の中でデータを削除する処理を書かなくてはならない。以下のようなコードが必要となる。

    def tearDown(self):
        [r.delete(r.id) for r in model.Record.select()]
        [r.delete(r.id) for r in model.Entry.select()]
        [r.delete(r.id) for r in model.RecordType.select()]
        [r.delete(r.id) for r in model.EnumRecordItem.select()]
        [r.delete(r.id) for r in model.DiaryUser.select()]
        testutil.database.commit_all()

このテストは幸い、DBはオンメモリで動いているので、速度はとっても速い。毎回INSERT/DELETEを繰り返しても、テストの実行時間に深刻な影響はないようだ。(とはいえ、テストケースが増えてきたときにどうなるかはまだ確認していない。)