コンストラクタインジェクションとフィールドインジェクション
- 最近?コンストラクタインジェクションが推奨される理由がしっかりと理解できた気がするので自分の考え・理解をまとめておこうと思い、この記事を書くことにしました。
- 以下のコードは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と関係のないテストで「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)
が必要になることです。