コンストラクタインジェクションが推奨されている理由を自分なりにまとめてみる


コンストラクタインジェクションとフィールドインジェクション

  • 最近?コンストラクタインジェクションが推奨される理由がしっかりと理解できた気がするので自分の考え・理解をまとめておこうと思い、この記事を書くことにしました。
  • 以下のコードはSpringBoot, Java, JPAを使って実装していることを想定しています。

フィールドインジェクションの利点と欠点

  • メリット
    • フィールドインジェクションの場合はフィールドの上に@Autowiredを付けるだけなのでインジェクトするのが楽。
    • インジェクトするものが増えてもフィールドを追加して@Autowiredを付けるだけで済む。
  • デメリット
    • テストが辛くなる。(後述)

フィールドインジェクションでの実装例

@Service
public class BookService{

    @Autowired
    private BookDao bookDao;

    @Autowired
    private HogeDao hogeDao;

    /**
     * 引数のbookIdと一致したBookを返す
     */
    public Book selectById(int bookId){
        Optional<Book> wrappedBook = bookDao.findById(bookId);
        return wrappedBook.orElseThrow(() -> NotFoundException("無い"));
    }
                          .
                          .
                          .
}

コンストラクタインジェクションの利点と欠点

  • メリット
    • 記述量が増えるというデメリットはありますが、フィールドをImmutableにできる。
    • テストがしやすくなる。(後述)
  • デメリット
    • コンストラクタインジェクションの場合はフィールドインジェクションのよりも記述量は増える。
    • インジェクトするものが増えたとき、フィールドインジェクションと比べると変更を加えるのが少し面倒。

コンストラクタインジェクションでの実装例

@Service
public class BookService{
    private final BookDao bookDao;
    private final HogeDao hogeDao;

    @Autowired // → Spring4.3以上ならこのアノテーション省略可
    public BookService(BookDao bookDao, HogeDao hogeDao){
        this.bookDao = bookdao;
        this.hogeDao = hogedao;
    }

    /**
     * 引数のbookIdと一致したBookを返す
     */
    public Book selectById(int bookId){
        Optional<Book> wrappedBook = bookDao.findById(bookId);
        return wrappedBook.orElseThrow(() -> NotFoundException("無い"));
    }
                          .
                          .
                          .
}

コンストラクタインジェクションによるテスタビリティの向上

  • 前述のとおりコンストラクタインジェクションにすることによってテストがしやすくなります。

フィールドインジェクションで実装されているクラスをテストする場合

  • @AutowiredでインジェクトされたフィールドはSpringBootが提供している@MockBeanを使うことでモックすることができます。
  • SpringBootが提供しているアノテーションを使用するので@ExtendWith(SpringExtension.class)@SpringBootTestが必要になります。
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class BookServiceTests{

    @Autowired    
    private BookService bookService;

    @MockBean
    private BookDao bookDao;

    @MockBean
    private HogeDao hogeDao;


    @Test
    public void 指定したbookIdの本を取得できること(){
        // setUp
        var expected = new Book(1, "本の名前"); 
        when(bookDao.findById(1)).thenReturn(expected);
        var bookService = new BookService();

        // execute
        var actual = bookService.selectById(1);

        // verify
        assertThat(...以下省略
    }
}
  • 一見、@MockBeanを付けるだけで済むのでとても楽に見えるのですがSpringBootTestはユニットテストの割にはかなり時間がかかります...。
  • Failed to create application contextというエラーが多発します。(経験済み)
    • application.yml(.properties)に記述しているDB情報が誤っていたりすると起こったり。
  • DBが動いていないとそもそもテストが走らない
    • DBが動いていないことが原因ではなくて@Tableでテーブルがない場合は自動生成するような実装にしていることが原因...?
  • DBと関係のないテストで「DBが...」というエラーが出てストレスがすごい(すごい)

コンストラクタインジェクションで実装されているクラスをテストする場合

  • @BeforeEachメソッドでmockする必要があるのが少し手間かもしれません。
  • フィールドインジェクションで実装されたテストでは@ExtendWith(SpringExtension.class)@SpringBootTestが必須でしたが、こちらでは付ける必要がありません。

public class BookServiceTests{ 
    private BookService bookService;
    private BookDao bookDao;
    private HogeDao hogeDao;

    @BeforeEach
    public void setUp(){
        bookDao = Mockito.mock(BookDao.class);
        hogeDao = Mockito.mock(HogeDao.class);
        bookService = new BookService(bookDao, hogeDao);
    }

    @Test
    public void 指定したbookIdの本を取得できること(){
        // setUp
        var expected = new Book(1, "本の名前"); 
        when(bookDao.findById(1)).thenReturn(expected);
        var bookService = new BookService();

        // execute
        var actual = bookService.selectById(1);

        // verify
        assertThat(...以下省略
    }
}
  • SpringBoot関連のアノテーションを付与していないのでテストがスムーズに行われます。
  • Failed to create application contextや「DBが...」といったエラーにも悩まされることがなくなり、ストレスなくテストをすることができます

  • @BeforeEachを付けたメソッドで自分でmockするのが面倒な場合はMockitoが提供している@Mockを付与する事で簡単にmockできます。

public class BookServiceTests{ 
    private BookService bookService;

    @Mock
    private BookDao bookDao;
    @Mock
    private HogeDao hogeDao;

    @BeforeEach
    public void setUp(){
        bookService = new BookService(bookDao, hogeDao);
    }
    // 以下省略
  • 注意点としては@RunWith(MockitoJUnitRunner.class)が必要になることです。

参考