依存性の注入、英語では Dependency Injection 、通称 DI と呼ばれる。コードを書く時に単体テストをしやすくする仕組みである。
単体テストとは
ユニットテストと言われるやつです。以下のようなコードがあったとしましょう。*1
<?php class BookDao { public function getBook($id) { $db = new Database(); // DBに接続 $db->connect('book_database'); $result = $db->query("SELECT * FROM book WHERE id = $id"); return $result; } }
このクラスの getBook
は単体テストができません。*2 なぜなら、 getBook
を呼ぶとDBに接続しにいってしまうからです。
仮にテストを書くとこうなると思います。
<?php class BookTest extends TestCase { public function test() { $bookDao = new BookDao(); $this->assertSame(['id' => '1', 'title' => 'book1のタイトル'], $bookDao->getBook(1)); } }
しかしこれは、 getBook
の中で実際にDBに接続しにいってしまうので、DBと結合した「結合テスト」です。単体テストではありません。
ではどうやって単体テストするのか
ではどうやって単体テストをするのか。依存性の注入を使って単体テストをするなら、まず BookDao
クラスをこう書き換えます。
<?php class BookDao { private $db; public function __construct() { $this->db = new Database(); } public function setDb($db) { $this->db = $db; } public function getBook($id) { // DBに接続 $this->db->connect('book_database'); $result = $db->query("SELECT * FROM book WHERE id = $id"); return $result; } }
setDb
メソッドによって外から $db
を注入できるようになりました。これを使った単体テストはこうです。
<?php class DatabaseMock { public function connect($dbname) { // モックなのでなにもしない } public function query($sql) { if($sql === "SELECT * FROM book WHERE id = 1") { return ['id' => '1', 'title' => 'book1のタイトル']; } else { return []; } } } class BookDaoTest extends TestCase { public function test() { $dbMock = new DatabaseMock(); $bookDao = new BookDao(); $bookDao->setDb($dbMock); $this->assertSame(['id' => '1', 'title' => 'book1のタイトル'], $bookDao->getBook(1)); } }
Database
クラスは実際にDBに接続しに行ってしまうので、 DatabaseMock
というクラスをテストの中で定義します。これを setDb
を使って無理やりいれてやることで、DBに接続しに行かなくなり、これは単体テストとなります。 BookDao
クラスが Database
クラスに「依存」していた部分を setDb
で「注入」できるようにしたのでこれが「依存性の注入」です。
このテストで確認できるのは(分かりやすくざっくり言うならば)以下のことです。
- BookDao は
getBook
メソッドで渡された引数$id
を「正しく」sql文に変換した上でDatabase
クラスのquery
メソッドに渡している。 Database
クラスがが返してきた結果をそのままreturnする
逆にテスト出来ないのは以下のことです。
connect
メソッドが呼ばれているかどうか($db->connect
メソッドが呼ばれていなくてもテストが落ちない)connect
メソッドが正しい引数で呼ばれているかどうか
これはコード(の DatabaseMock
あたりの実装)を読むとなんとなく分かっていただけるかと思います。
まずは、なんとなく、「依存性の注入」と「単体テスト」の関係について分かっていただけたでしょうか。
このテストの書き方はあまりいい書き方ではない
さて、この書き方はあまりいい書き方ではありません。が、依存性の注入が何か、ということを分かりやすく説明するためにこのような書き方にしました。
なぜこの書き方が良くないのか。
Database
クラス以外に依存するものが増えてきた場合に、その都度DatabaseMock
,XxxxxMock
を実装していかなければならないconnect
メソッドが正しく呼ばれてからquery
メソッドが正しく呼ばれていることをチェックしたい といった場合に更に実装が複雑になる- とにかく、
BookDao
が複雑になればなるほど100倍くらいの勢いでテストも複雑になっていく
ではどうやったら良い書き方になるのか
- 今回の依存性注入方法は、
setDb
というセッターを使った注入方法なので、「セッターインジェクション」と言われます。一般的に使われているのは「コンストラクタインジェクション」、つまりコンストラクタで依存を注入する方法ですので、こちらを使います。 - PHPで言えば Mockery のような、モック作成用のライブラリを使うことで、
DatabaseMock
のようなクラスの作成を楽に行います。 - AuraDi のような、Dependency Injection 用のライブラリを使います。
さて、本当はAuraDiの説明をしたかったのですが、AuraDiの説明をするためにはDIの説明をしなくちゃいけないということで軽くDIの説明をしました。AuraDiというPHPのDIライブラリ、非常にいいライブラリなのですが、結構使い方が複雑だったりします。AuraDi は コンストラクタインジェクションと深く関係してくるので、次回書く機会があればコンストラクタインジェクションについて書いて、AuraDiの説明につなげていきたいと思います。