Edited at

Spring bootのテストでフィールドインジェクションしたクラスをSpring コンテナ使わずにテストする

ある程度のJavaの経験者であれば至極あたりまえのことなのですが、私と同じような問題をかかえ、「あ、そういえばそうか」的な感じの方もいらっしゃると思いますので書きます。ただのリフレクションの話です。

ただし、この方法がテスト設計として正しいとか正しくないかは別の知識になりますのであくまで方法の一つとしての記述になります。(アンチパターンの可能性もあります!)

書いててなんですが、今後はコンストラクタインジェクションを使用した設計で行こうと思ったきっかけとなった方法でもあります。


Spring の機能(コンテナ)を使ってテストするときの問題

私が従事しているプロジェクトではアプリの立ち上げにいろいろな情報を読み込むためか3分ほどかかり(貸与いただいているPCでは)テストが走るまでが遅いです。リズムよくユニットテストのサイクルしたいのに、Springコンテナを使ったテスト毎にこの時間はがかかるので、テストしたくない理由に十分なものでした。

たとえば、以下のようにテストでSpringコンテナを呼び出すとき(Spring boot ver 1.Xです。)


 テスト対象クラス


@Service
public class ParentService {

@Autowired
ChildService childService;// フィールドインジェクション

public SampleForm generateSample(int n) {
//ここで注入
int calNumber = childService.calculate(n);
return this.prepared(calNumber);
}

private SampleForm prepared(int calNumber) {
int newN = calNumber * 2;
SampleForm sample = new SampleForm();
sample.setCalNumber(newN);
return sample;
}

}


テスト


@ActiveProfiles("test")
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestConfig.class)
@WebIntegrationTest(randomPort = true)
@Transactional
public class SampleTest {

@Autowired
ParentService parentService;

@Test
public void generateメソッドに2を与えると4を持ったSampleFormが得られる() {
SampleForm sample = parentService.generateSample(2)
assertThat(sample.getCalNumber(), is(4));
}
}

のように行いますが、

たとえばこの程度の短いテストを行う毎回に数分待つのではやってられません。

そこでいままでは例えば上記のような場合は ParentService.prepared

メソッドをpublicにして


//Springコンテナ機能を使わない。
public class SampleTest {

ParentService parentService = new ParenteService();

@Test
public void preparedメソッドに2を与えると4を持ったSampleFormが得られる() {
SampleForm sample = parentService.prepared(2)
assertThat(sample.getCalNumber(), is(4));
}
}

にしたり、なるべくメソッド内部でフィールドインジェクションのメソッドを呼ばないようにしたり、依存オブジェクトを引数にして渡したりするなどしてPOJOでテストできるようにしていました。フィールドインジェクションして呼び出したクラスはSpringコンテナを使用しないと注入されないためそのままPOJOとしてnewしたものにはnullでセットされます。以下のように書いた場合は

ParentService.generateSample 内で呼び出したchildeService.caluclateの時NullPointerExceptionが発生します。


public class SampleTest {

ParentService parentService = new ParentService();

@Test
public void generateメソッドに2を与えると4を持ったSampleFormが得られる() {
/*
generateSampleメソッド内でchildService.caluclateを呼び出すが、このテストでは
new ParentService()としているためchildServiceはnullなので例外が発生する
*/

SampleForm sample = parentService.generateSample(2)//Exception!
assertThat(sample.getCalNumber(), is(4));
}
}

ChildService.caluclateメソッドもpublicであり、別のテストでその検証を行い、ParentService.prepared もテストで検証ができれば

Parentservice.generateSample は正しいはずなのでテストしなくても正しいはずといえると思います(上記のような簡単な例であれば)。しかし、設計としてもともとParentService.preparedは外部から呼び出すメソッドとしておらず、テストのためにpublicにするのははたして正しいといえるのでしょうか?違う気がします。

また実際はこんな簡単な実装ではないのでやはりフィールドインジェクションにて注入したオブジェクトを持ったメソッドをテストしたいことが出てきます。

そこでふと思いました。

「動的言語であれば動的にフィールドにアクセスしてセットしたり、動的にメソッド定義できるのにな。。。まてよ?Javaにもリフレクションがあるな。」と。

普段Javaという言語では動的にフィールドを触ったりすることはありません。JavaScriptであれば動的にフィールド名を参照したりオブジェクトをメソッドを追加したりするのは当たり前の感覚で行えますが、Javaでの開発ではプロダクションコードに対して行うことは普通は(私の浅い知識の中では)ないと思います。理由はいろいろあると思いますがJavaの特性として静的型付けによる安全性が損なわれるというのが私の理解です。なのでリフレクションを使うという発想はまるでありませんでしたが上記のようなテストを行うために使えるんじゃないかと考えました。

そして以下のように書きました。


public class SampleTest {

ParentService parentService = new ParentService();

@Before
public void setUp() {
Class<?> c = parentService.getClass();
parentService = (ParentService)c.newInstance();
Field f = c.getDeclaredField("childService");
f.setAccessible(true);
f.set(parentService , new ChildService());

}

@Test
public void generateメソッドに2を与えると4を持ったSampleFormが得られる() {
/*
generateSampleメソッド内でchildService.caluclateを呼び出す。リフレクションで
でセットしているため以下のテストが可能である。
*/

SampleForm sample = parentService.generateSample(2)
assertThat(sample.getCalNumber(), is(4));
}
}

以上のように書くことでSpringコンテナを使わずにフィールドにアクセスしてセットし、

テストを行うことができます。よってテスト開始までの時間をまたずテストが可能になりました。

例ではnewしてChildServiceを渡していますが、振る舞いをもたせたmockをセットすることも可能です。

ただし、冒頭にも触れたように、そもそもインジェクションってなんだ?位の知識しかありませんでしたので上記方法を考えたことがきっかけになり、Springに関する本を読み返したり以下のブログを読んだりして設計を見直していこうと思うに至りました。

参考

SpringでField InjectionよりConstructor Injectionが推奨される理由

以上です。