猫でもわかるWeb開発・プログラミング

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

依存性の注入(Dependency Injection 通称 DI)とはなにか - 単体テストをしやすくするための「依存性の注入」

依存性の注入、英語では 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の説明につなげていきたいと思います。

*1:疑似コードなので超ざっくりです。injectionとかもかんがえてないです。

*2:正確には「すごく単体テストしづらい」