モックで置き換えの境界線
以前のSymfony勉強会の折にユニットテストとモックの話をさせていただいたのですが、その時に@cakephperさんから次のような意見を頂いていました。
APIを信用して基本Mockでという話、API側の仕様変更を常にウォッチしてないといけないのでフレームワークのアップデートとか大変なんじゃないのかなと思ったんだけど、そこんとこまだ消化できてない
twitter - @cakephper
まず最初に回答としては「APIが変わるのであれば概ねそのとおり」となります。しかし、これはモックを使うかどうかには関係ありません。ソフトウェアはフレームワークやライブラリ等サードパーティコードの仕様変更に常に影響を受けます。ここで、議論の前提を共有しておく必要があるでしょう。
- ユニットテストとファンクショナルテストの使い分け。どちらが書きやすいのか。
- ユニットテストでテストしたい箇所はどこか。
前提として、ユニットテストの方がファンクショナルテストよりも「気軽に書ける」「局所的な仕様を確認できる」ものだという点があります。TDD等を適用して継続的にソフトウェアを開発・成長させて行く場面では、個別のメソッドのロジックを局所的にユニットテストで検証できることを重要視しています。
これは、ファンクショナルテストが不要ということではありません。役割が違うということです。
原則としてモックするのは自分の持っている型だけ
書籍『実践テスト駆動開発』の第8章「サードパーティーコードの上に構築する」にて、「モックするのは自分の持っている型だけ」という節がでてきます。ここには次のように書かれています。
ふたつめのリスクは、私たちがスタブやモックで作り出す振る舞いを外部ライブラリの実際の振る舞いと正確に合致させなければならない点にある。これがどの程度難しいかは、扱うライブラリの質に依存する。私たちの書いたユニットテストが妥当であると自信を持って言えるくらいに分かりやすく説明されているか(そして、実装されているか)ということだ。いったん正しい状態に持っていったとしても、ライブラリをアップグレードする際には、テストが変わらず正しく動いていることを確認しなければならない。
実践テスト駆動開発 - p74 自分たちで変更できない型をモックしてはならない
自分の持っている型というのは、自分が書いたクラスということですね。外部のものに対して、基本的にはモックにしないということです。ただし、ここで前提となっているのはサードパーティコードのAPIの安定性がそれほど高いとは言えない場合です。この場合の対処として、実践テスト駆動開発ではアダプターレイヤーを作るよう書かれています。サードパーティコードの不安定性をアダプターレイヤーで吸収するわけです。これは、SOLID原則でいう依存関係逆転の原則(DIP)、および安定したパッケージへ依存するようにする安定依存の原則(SDP)を適用するということでもあります。
Note ここでいうAPIの安定性とは、ライブラリメソッド等の入出力仕様の安定度を指します。
フレームワークのコードは安定と見る
サードパーティのコードといえども、フレームワークやORMのようにインフラとして利用するコードの場合、一般的にはAPIの安定性は高いとみなせます。この場合、ユニットテストではサードパーティコードのオブジェクトを直接モックして使ってもよいでしょう。サードパーティコードのAPI変更については、自分の持っているクラスに対するファンクショナルテスト(インテグレーションテスト)でカバーします。この場合でも、テストする基準は自分の持っているクラスであり、サードパーティコードの仕様を網羅的にテストするわけではありません(テスト作成にかけられる手間やコストとも関係します)。
実際の例:Symfony
PHPメンターズのユーザー登録サンプルアプリケーションで、ドメインレイヤーのUserRegistrationServiceクラスのテストコードを例として紹介します。register()メソッドでユーザーを登録するユースケースシナリオを実行しますが、その時にインフラとして次の 3 つを利用しています。
- Doctrine\ORM\EntityManager
- Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface
- Symfony\Component\Security\Core\Util\SecureRandomInterface
EntityManagerは実クラスですが、ここは Doctrine\Common\Persistence\ObjectManager(インターフェイス)と同義です。このテストでは PasswordEncoderInterface 等のインターフェイスをモックして使っています。もちろん、PasswordEncoderInterface の実装の動作が将来変更になる可能性はゼロではありませんが、このユニットテストでテストしたいのは PasswordEncoder の結果ではなく、「register() メソッドが PasswordEncoder を使ってパスワードを生成していること」です。ですので、モックを使ってインタラクションをテストしているのです。
class UserRegistrationServiceTest extends ComponentAwareTestCase { /** * @test */ public function ユーザーを登録する() { $userClass = 'Example\UserRegistrationBundle\Domain\Data\User'; $userRepository = \Phake::mock('Example\UserRegistrationBundle\Domain\Data\Repository\UserRepository'); $password = 'PASSWORD'; $user = \Phake::mock($userClass); \Phake::when($user)->getPassword()->thenReturn($password); $entityManager = \Phake::mock('Doctrine\ORM\EntityManager'); \Phake::when($entityManager)->getRepository($userClass)->thenReturn($userRepository); $this->setComponent('example_user_registration.entity_manager', $entityManager); $passwordEncoder = \Phake::mock('Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface'); \Phake::when($passwordEncoder)->encodePassword($this->anything(), $this->anything())->thenReturn($password); $this->setComponent('example_user_registration.password_encoder', $passwordEncoder); $activationKey = 'ACTIVATION_KEY'; $secureRandom = \Phake::mock('Symfony\Component\Security\Core\Util\SecureRandomInterface'); \Phake::when($secureRandom)->nextBytes($this->anything())->thenReturn($activationKey); $this->setComponent('security.secure_random', $secureRandom); $userTransfer = \Phake::mock('Example\UserRegistrationBundle\Domain\Data\Transfer\UserTransfer'); \Phake::when($userTransfer)->sendActivationEmail($this->anything())->thenReturn(true); $this->setComponent('example_user_registration.user_transfer', $userTransfer); $this->createComponent('example_user_registration.user_registration_service')->register($user); \Phake::verify($secureRandom)->nextBytes($this->isType(\PHPUnit_Framework_Constraint_IsType::TYPE_INT)); \Phake::verify($user)->setActivationKey($this->equalTo(base64_encode($activationKey))); \Phake::verify($user)->getPassword(); \Phake::verify($passwordEncoder)->encodePassword($this->isType(\PHPUnit_Framework_Constraint_IsType::TYPE_STRING), $this->isType(\PHPUnit_Framework_Constraint_IsType::TYPE_STRING)); \Phake::verify($user)->setPassword($this->isType(\PHPUnit_Framework_Constraint_IsType::TYPE_STRING)); \Phake::verify($user)->setRegistrationDate($this->isInstanceOf('DateTime')); \Phake::verify($userRepository)->add($this->identicalTo($user)); \Phake::verify($entityManager)->flush(); \Phake::verify($userTransfer)->sendActivationEmail($this->identicalTo($user)); } }
実際には
実際のところ、サードパーティライブラリの安定度を個別に全て、使う前に評価するというのも難しいことです。ある程度信頼できるフレームワークであればまずはユニットテストなどではモックする。運用していく中で壊れやすい部分が判明した時点で、アダプターレイヤー等を設け安定性を確保する、という方針が現実的でしょう。
まとめ
Q.「フレームワークのAPI変更を細かくウォッチしていなければいけないのか?」
A. 前述のように、モックを使おうが使うまいが、フレームワークやライブラリ等のサードパーティコードの仕様変更に対応しなければならないことに変わりはありません。ユニットテストを修正する分の手間は増えますが、モックによって自身のコードのロジックに焦点を絞ることができるメリットはそれを補って余りあると考えています。
参考
モックするのは自分の持っている型だけ 『実践テスト駆動開発』 p.74〜「自分たちで変更できない型をモックしてはならない」「アダプタ層を書く」
この書籍では、モックを使ったTDDについて詳細かつ実践的な解説が書かれています。 依存関係逆転の原則 DIP:Dependency Inversion Principle 『アジャイルソフトウェア開発の奥義 第2版』 p.163〜 安定依存の原則 SDP:Stable Dependencies Principle 『アジャイルソフトウェア開発の奥義 第2版』 p.325〜