Born Too Late

Yuya's old tech blog.

第56回PHP勉強会@関東で PHPUnit について話してきた

2011-10-02 20:36:30
PHPUnit でよりよくテストを書くために
View more presentations from Yuya Takeyama

最近 HTML5 化した Slideshare ですが, エラーで表示できないスライドが多すぎてまともに閲覧できないようです.
この記事に掲載している Flash 版は今まで通りの問題なく表示できるのですが...

スライドが完成したのが勉強会への出発 15 分前で, 通しで練習することすらできず, 発表はかなりひどいものとなっていまいました.
最低限, スライドの流れぐらいは頭の中に入れて発表すべきですね…

スライドだけ見てわかるような作りにはなっていないので, 以下で補足したいと思います.
ただし, 序盤は省略し, 本論となる書法編とパターン編についてのみとなります.

それぞれ該当するスライド番号も付記してありますので, よければご活用ください.

書法編 1: ヘルパーメソッドを使う (25 ~ 30)

テスト対象のオブジェクト, それを生成するのに必要な依存オブジェクトの生成には, ヘルパーメソッドの使用を検討しましょう.
new するだけで済むものであればそれでもいいのですが, 生成に複雑なステップを踏む必要がある場合に威力を発揮します.

これは一般的なプログラミングにおけるサブルーチンみたいなものだと思って問題無いと思います.
ただし, 可読性を問題にするのであれば, 多少長くても説明的でわかりやすいメソッドの命名を心がける必要があります.

テスト対象の生成が複雑だと, 複数のテストの比較が大変になります.
ヘルパーメソッドを使うことで, テスト A とテスト B はどこに有意な違いが有り, 結果に違いが生まれるのか, ということがわかりやすくなります.

書籍 では, Fixture Setup Patterns において Creation Method という名前で紹介されています.
また, これの応用として, 引数付きの Parameterized Creation Method というものも紹介されています.

書法編 2: テストメソッドを分割する (31 ~ 36)

ひとつのテストメソッド内で, いろんなことをテストしようとすると, 何をテストしようとしているのかがわかりにくくなります.
例え同じメソッドを対象にしたテストであっても, 前提条件等が違うのであれば, 分割するべきです.

分割することで, それぞれのテストメソッドには, より具体的な名前をつけることができます.
これもまた可読性に寄与すると言えるでしょう.

これは癖のレベルの話なので, 普段からテストを書いている人であれば無意識にそうなると思います.
まずは 1 つのテストメソッドにおいてアサーションは 1 つまで, というルールを課すことで自然とそういう書き方に近づくと思います.

パターン編 1: 依存性は外から差し込む (39 ~ 49)

スライドでは, フレームワークによくある Request クラスを例に, $_SERVER といったスーパーグローバル変数を使ったときに起こる問題について説明しました.

$_SERVER を使った実装でもちゃんと動作するので, 問題と見なさないこともできます.
しかし, 問題はテストを書こうとしたときに顕在化します.

スーパーグローバル変数に限らず, グローバル変数を利用した実装だと, 複数のテストが影響し合う可能性が生まれます.
スライドではコードで例示しているので, 具体的にはスライドの該当箇所を参照してください.
スライドのように unset($_SERVER['HTTPS']) とすることで回避できますが, コードベースが巨大化すると, 暗黙的なコンテキストが肥大化し, テストを書くのがだんだんと困難になります.

その代わりにどうするのかというと, スーパーグローバル変数 $_SERVER を直接参照するのではなく, コンストラクタやセッターメソッド等で, 引数として差し込む, という方法を採ります.
Dependency Injection (DI, 依存性の注入) というパターンです.
DI を採用することで, 再利用性が高くなるので, テストを書くことで設計が向上する例としては比較的よく挙げられます.

パターン編 2: 外部への依存を避ける (50 ~ 62)

ひとつ前の話を逆に言っているだけですが, 別の側面から解説しています.

外部への依存としては, 例えば以下のようなものが挙げられます.

しかし, もちろんこれらを完全に避けてのアプリケーション開発は考えられません.
それではどうするのか, ということで, スライドでは Active Record の問題点と, それを解決したパターンとしての Data Mapper を例に説明しています.

ここでいう Active Record というのはパターンとしての Active Record で, 書籍 で紹介されています.
Ruby on Rails の ORM としての ActiveRecord は, それを実装したものです.

Active Record においては, 1 つのレコードが 1 つのオブジェクトにマッピングされ, それぞれのレコードはデータベースを知っています.
なので, $record->save() といった操作が可能になります.

Data Mapper でも 1 つのレコードが 1 つのオブジェクトに, という部分は変わりませんが, それぞれのレコードはデータベースのことを知りません.
例えば users というテーブルであれば, User クラスがその 1 レコードを表し, UserMapper はテーブルを表します.
(実際にはテーブルをラップしたクラスとして実装されることが多いと思いますが, ここでは単純化しています)
レコードはデータベースのことを知らないので, レコードを保存する際は $mapper->save($record) といったようにする必要があります.

それでは Active Record の何が問題なのか.
Active Record ではレコードがドメインロジック (ビジネスロジック) を持ったドメインオブジェクトとしての側面と, データアクセスを行うオブジェクトとしての側面を持つことになります.
それぞれは全く違った関心事なので, 関心事の分離 (SoC)単一責任原則 (SRP) といったソフトウェア開発の原則に違反することになります.
テストに関していうと, データベースへのアクセス無しにドメインロジックをテストできない, といったことが起こりやすくなります.
(もちろん, 実装によりますが)

逆に Data Mapper ではドメインロジック (上の例でいうと User クラス) をデータベース無しでテストできます.
データベースに対してテストを書きたいときもありますし, そのためのテクニックもいろいろとありますが, まずはそれらを無視してテストできるようなクラス分割を行うことで, 低コストで確実にテストを書くことができます.

パターン編 3: Singleton を避ける (63 ~ 71)

Singleton は GoF のデザインパターンの中では最も理解しやいもののひとつで, よく使われるパターンのひとつです.
しかし, 同時に批判的意見の多いパターンでもあります.

Singleton ではクラスの static プロパティとしてオブジェクトとその状態がグローバル空間に残ってしまいます.
これはつまり, グローバル変数と同様の性質を持っている, ということができます.

また, グローバル変数と違って, 容易に上書くこともできないので, テストをする上ではさらに厄介な存在といえます.

対策としては以下が考えられます.

また, PHPUnit にはこれらを回避するための --process-isolation オプションであったり, @runTestsInSeparateProcesses アノテーションといった機能もあるのですが, それらについては説明しませんでした.
Singleton を使わなければ, これらの機能について理解する必要もなくなりますし, これらは Slow Test (テストの実行が遅くなる) 問題を引き起こします.

Singleton で実装したくなったとき, 本当に Singleton である必要があるのかについては, 一度考えてみる必要があるでしょう.

参考文献

スライド中に挙げたものです.
書籍へのリンクにはアフィリエイトタグをつけています.

こちらはスライドです.