9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

コンストラクタインジェクションだと単体テストが楽なんじゃない?って話

Last updated at Posted at 2020-02-05

はじめに

Springでコード書いてるときに絶対使うアレ、@Autowiredです。
最近はフィールドインジェクションじゃなくて、コンストラクタインジェクションのほうが推奨されているらしいのです。

そもそもなんでコンストラクタインジェクションが推奨されているのかは、ググってみてください。
いろいろ理由がありますが、その中で一番納得したのは「あれ?これって単体テスト楽なんじゃね??」って思ったので、それについて書いてみました。

モック作るのめんどくさい

テスト対象のクラス

サンプルが適当すぎですが、こんなメールを送るクラスがあったとして。

MailService.java
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やらなんやらがちゃんと合ってるか?

というテストコードを書いてみます。

MailServiceTest.java
@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とか毎回書き方忘れるしコメントないと(コメントあっても)絶対あとで読めないし、なんかそもそもめんどくさい・・・。

コンストラクタインジェクションにしてみる

テスト対象のクラス(修正箇所だけ)

MailService.java
public class MailService {

    private final MailSender mailSender;

    @Autowired
    public MailService(MailSender mailSender) {
        this.mailSender = mailSender;
    }

    // 以下略

テストコード

  • テスト用のモッククラスとして独自で実装したものを突っ込めるので、テスト対象と関係ないクラスの動作を好きなようにできる。
  • モックの動作が、Javaのコードとして見える。
  • @RunWith@SpringBootTestもいらない。
MailServiceTest.java
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から結合テストっぽくテストコード書きたいけど邪魔なクラスある!なんて時は、このやり方も良いと思います。

MailServiceTest.java
@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);
    // 略
9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?