Javaにおける依存性の分離
Javaにおいて依存性の分離がどのように実現されるかを、簡単な例を通じて説明します。今回使用するサンプルコードはGitHubで公開されています。
今回テストするユニットは図4に示す依存性を持つLogicクラスです。Logicクラスは、指定された組織名のレポジトリ情報をGitHubに問い合わせ、取得したレポジトリのうち最新のものを返すという処理を行います。レポジトリ情報をGitHubに問い合わせる処理は、依存性のGithubApiクラスを通じて実現します。このGithubApiクラスは、テスト対象のLogicクラスが依存しているため、モック対象になるわけですね。
以下にLogicクラスのコードを示します。このクラスでは、コンストラクタ注入(constructor injection)という方法でDIを実現しています。つまり、コンストラクタの引数で依存性を受け取り、受け取った依存性を使うようにクラスを初期化するのです。他にもsetter注入やフィールド注入といった方法がありますが、この記事ではコンストラクタ注入だけを扱います。この例では、依存性はprivateはフィールドとして定義されており、コンストラクタの引数でそのフィールドを初期化しています。このとき、フィールドの型はインターフェースとか抽象クラスである必要はありません。普通の具象クラスで良いのです。具象クラスにするのがベストプラクティスというわけでは決してありませんが、具象クラスであっても、モックフレームワークがそのクラスを継承したクラスを実行時に生成してくれるので、テストの観点からは問題になりません。
public class Logic {
private final GithubApi githubApi;
private Logic(GithubApi githubApi) {
this.githubApi = githubApi;
}
public Logic() {
this.githubApi = new GithubApi();
}
public Repository latestRepository(String organizationName) {
List<Repository> repositories = githubApi.getRepositories(organizationName);
return repositories.stream() ...
}
}
DI用のコンストラクタはprivateで定義しておき、本番コードで使ってほしい、引数を取らないコンストラクタを別に用意しておきます。DI用とそれ以外のコンストラクタを用意しておくことが重要なポイントです。DI用のコンストラクタしか作らないと、アプリケーションが肥大化したときに大変なことになります。今は、Logicクラスに依存するクラスのことは考えていませんが、もしLogicクラスに他のクラスが依存し、そのクラスが更に異なるクラスから使われたらどうなるでしょうか? これらの上位クラスにもDIを適用したいとしたら? 依存性階層の最上位にあるクラスが、あらゆるクラスの全ての依存性を提供しなくてはなりませんね。それは大変なことです。
ここでのDIの目的は、本番時とテスト時で依存性の実装を切り替えることであって、本番時に異なる依存性を複数提供することではないので、実装時の依存性は決め打ちで良いのです。このあたりの工夫は、SpringのようなDIコンテナを使うともっと美しくなるのですが、今回はフレームワーク非依存の手法を紹介しています。
上述のことを考えると、DI用のコンストラクタというのは本番コードからは使ってほしくないのです。誤った依存性を渡されたりしたくありません。そういった誤った使用を防ぐためにもprivateにしています。privateにしても、モックフレームワークがリフレクションを使ってprivateなコンストラクタを使ってくれるので、テスト時にDIできないということはありません。
テストコードのサンプルに移りましょう。以下にLogicクラスのテストコードを示します。このテストコードでは、テストフレームワークにJUnit4を、モックライブラリにmokitoを使用しています。@Mockと@InjectMocksというアノテーションを使って、どのフィールドがモックで、どのフィールドが注入対象かを宣言します。MockitoAnnotations.initMocksというメソッドを呼び出すと、アノテーションが付与されたフィールドが初期化されます。このとき、mockitoはコンストラクタ、setter、フィールドの順に注入を試みます。今回の例では、注入に使えるDI用privateコンストラクタがあるので、それが使用されます。
public class LogicTests {
@Mock
private GithubApi githubApi;
@InjectMocks
private Logic logic;
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
@Test
public void latestRepositoryReturnsLatestRepository() {
when(githubApi.getRepositories(anyString())).thenReturn(returnedByMock);
...
veryfy(githubApi, times(1)).getRepositories(eq(organizationName));
}
}
Mockitoを使う場合、モックの振る舞い設定や呼び出しの検証はstaticメソッドを通じて行います。when(mock.method(...)).thenReturn()という構文を使ってモックの振る舞いを設定し、veryfyメソッドを使ってモックが正しい引数で呼び出されたか検証しています。
このJavaの例では、依存性の注入先としてコンストラクタやsetter、フィールド注入などを定義する必要がありますが、安全のために、これらの可視性をprivateにしてもテストには支障がないことを示しました。また、モック対象のクラスに大きな制限はなく、インターフェースや抽象クラスを導入する必要がないことも示しました。今回はMockitoというモックライブラリを使いましたが、ライブラリによってはstaticメソッドすらモックしてしまうことができます。このような恩恵は、Javaが長い歴史の中で築いてきたリフレクションやバイトコード操作という機能と、それを活用したエコシステムという資産がもたらしたものです。
