はじめに
Spring Framework(boot)で、DIのメリットである「テスタブルなコード」にしようと思いながら、インジェクションするクラスに全く手を加えずに実装するってどうやるんだろう?と思ったのがきっかけで、自分なりに考えてみたのでその内容について書きたいと思います。
前提
Springって何?DIって何?って話はしません。こちらの説明が素敵なので参考にしてください。冒頭記載のある勉強会、私も参加しましたが良い勉強になりました。
疑問に思っていたこと
テスタブルなコードにするといっても、インジェクションされるクラスが未実装のためテスト用にスタブクラスを作ったとして、そのクラスが出来上がったら少なくともインジェクションしている箇所を書き換えなくてはいけないんだよなって思っていました。
例えばこんな感じ。
@RestController
public class DemoController {
@Autowired
DemoServiceStub demoService;
//DemoService demoService; //テストのためにここのコメントアウトを切り替える
@GetMapping("/echo")
public String echo() {
return demoService.getMessage();
}
}
public class DemoService {
String getMessage(){
//まだ実装中
return "xxx";
}
}
public class DemoServiceStub {
String getMessage(){
return "foo is stub";
}
}
コメントで書いたとおり、インジェクションしているところを正規のクラスとスタブクラスとで切り替えないといけない。特定のメソッドだけテストしたいなら影響ないと思いつつも、やっぱりフィールドいじっちゃってるし気持ち悪いなという感覚。
で、考えて直したのがこちら。
@RestController
public class DemoController {
@Autowired
@Qualifier("demoservice") //インジェクション対象を名前指定する。正規とスタブで切り替えない。
DemoService demoService;
@GetMapping("/echo")
public String echo() {
return demoService.getMessage();
}
}
//インターフェースを1つ作る
public interface DemoService {
String getMessage();
}
@Service(value = "demoserviceWIP") //名前を付ける。スタブとかぶらないように。
public class DemoServiceImpl implements DemoService {
public String getMessage() {
//まだ実装中
return "xxx";
};
}
@Service(value = "demoservice") //名前を付ける
public class DemoServiceImplStub implements DemoService {
String getMessage(){
return "foo is stub";
}
}
こうすることで、正規のときとスタブのときでテスト対象のソース(この場合はDemoController)に手を加える必要がなくなりました。
このやり方は、インターフェースで実装したいクラスが複数あった場合にも有効な気がします。
SpringではBean登録する型(クラス)が同じものが複数あった場合はどちらを登録すればいいのか判断がつかないため、こうやって名前解決をするみたいなので。
(というか、どちらかというとこの仕組みから今回の件のヒントをもらった感じです。)
おさらい
- 正規クラスとスタブクラスを共通的にするためにインターフェースを使って実装する
- これはDIとしてそもそも最低限やらなきゃいけないことな気がします
- インジェクションされる側のクラスに別名を与える(
@Service
のvalue属性) - インジェクションする側で名前解決する(
@Qualifier
) - インジェクションする側はインジェクションされる側の実装状況に関わらずいっさい手を加えない
- インジェクションされる側でインジェクション名を切り替える
最後に
ググってもほぼ出てこなかったやり方です。当たり前すぎるかアンチパターンになっているかもしれません。
その場合はご指摘頂けたらと思います。