はじめに
Springでコード書いてるときに絶対使うアレ、@Autowired
です。
最近はフィールドインジェクションじゃなくて、コンストラクタインジェクションのほうが推奨されているらしいのです。
そもそもなんでコンストラクタインジェクションが推奨されているのかは、ググってみてください。
いろいろ理由がありますが、その中で一番納得したのは「あれ?これって単体テスト楽なんじゃね??」って思ったので、それについて書いてみました。
モック作るのめんどくさい
テスト対象のクラス
サンプルが適当すぎですが、こんなメールを送るクラスがあったとして。
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
@RestController
@RequestMapping(path="/mail")
public class MailService {
@Autowired
MailSender mailSender;
@PostMapping
public void send() {
SimpleMailMessage msg = new SimpleMailMessage();
msg.setFrom("noreply@example.com");
msg.setTo("to@example.com");
msg.setSubject("Hello World");
msg.setText("Welcome to my world.");
mailSender.send(msg);
}
}
テストコード
- 単体テストなので、実際にメールは送らない。
- MailSenderに渡してるFromやらなんやらがちゃんと合ってるか?
というテストコードを書いてみます。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class MailServiceTest {
@InjectMocks
MailService mailService;
@Mock
MailSender mailSender;
@Captor
ArgumentCaptor<SimpleMailMessage> msgCaptor;
@Test
public void test() {
// テスト対象メソッドの実行
mailService.send();
// テスト対象メソッドの中で、mailSender#sendが1回実行されていること
// ついでに引数をキャプチャ
verify(mailSender, times(1)).send(msgCaptor.capture());
// 引数の中身を検証
SimpleMailMessage msg = msgCaptor.getValue();
assertThat(msg.getFrom(), is("noreply@example.com"));
assertNotNull(msg.getTo());
assertThat(msg.getTo().length, is(1));
assertThat(msg.getTo()[0], is("to@example.com"));
assertThat(msg.getSubject(), is("Hello World"));
assertThat(msg.getText(), is("Welcome to my world."));
}
}
フィールドインジェクションしているクラスだと、こんな書き方でしょうか。
慣れていればいいですが、Captorとか毎回書き方忘れるしコメントないと(コメントあっても)絶対あとで読めないし、なんかそもそもめんどくさい・・・。
コンストラクタインジェクションにしてみる
テスト対象のクラス(修正箇所だけ)
public class MailService {
private final MailSender mailSender;
@Autowired
public MailService(MailSender mailSender) {
this.mailSender = mailSender;
}
// 以下略
テストコード
- テスト用のモッククラスとして独自で実装したものを突っ込めるので、テスト対象と関係ないクラスの動作を好きなようにできる。
- モックの動作が、Javaのコードとして見える。
-
@RunWith
も@SpringBootTest
もいらない。
public class MailServiceTest {
/**
* MailSenderのモッククラスを独自に作っちゃう
*/
private class MailSenderMock implements MailSender {
SimpleMailMessage simpleMessage;
@Override
public void send(SimpleMailMessage simpleMessage) throws MailException {
this.simpleMessage = simpleMessage;
}
@Override
public void send(SimpleMailMessage... simpleMessages) throws MailException {
}
}
@Test
public void test() {
// モッククラスを渡してインスタンスを作る
MailSenderMock mailSenderMock = new MailSenderMock();
MailService mailService = new MailService(mailSenderMock);
// テスト対象メソッドの実行
mailService.send();
// 検証
SimpleMailMessage msg = mailSenderMock.simpleMessage;
assertThat(msg.getFrom(), is("noreply@example.com"));
assertNotNull(msg.getTo());
assertThat(msg.getTo().length, is(1));
assertThat(msg.getTo()[0], is("to@example.com"));
assertThat(msg.getSubject(), is("Hello World"));
assertThat(msg.getText(), is("Welcome to my world."));
}
}
サンプル上、モッククラスはインナークラスとして書いてますが、個別にクラスファイル作っても良いです。
Mockito.mockで作ってもいいですけどね。Mockitoめんどくさい、って言っていたのが本末転倒ですが・・・
(何もしなくて良い、ってクラスならMockito.mock()でも良いかも。)
サンプルのテスト対象クラスが簡単すぎるので大差ないようにも見えますが、Mockitoでモック化しなくていいのは楽だなぁと思いました。
また、SpringBootに依存しなくなるので、プロパティの設定やDBが無いなどの理由でJUnitが動かないというのも無くなります。
こんなことも
例えば3つくらい@Autowired
していて、1つだけ単体テスト用に動作を変えるようなこともできます。
この場合は、@SpringBootTest
にして、動作は変えないクラスをテストクラスの中で@Autowired
します。
この場合はSpringBootに依存しちゃいますけどね。
Controllerから結合テストっぽくテストコード書きたいけど邪魔なクラスある!なんて時は、このやり方も良いと思います。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class MailServiceTest {
@Autowired
HogeSerice hogeService;
@Autowired
FugaService fugaService;
private class MailSenderMock {
// 略
}
@Test
public void test() {
// モッククラスを渡してインスタンスを作る
MailSenderMock mailSenderMock = new MailSenderMock();
MailService mailService = new MailService(mailSenderMock, hogeService, fugaService);
// 略