これはなんですか
Java - Spring Boot の単体テストについて、勉強会の資料。しゃべることをまとめたもの。
世間一般で言うところの単体テスト
単体テスト(ユニットテスト、略してUTと呼ばれることもあります)は、プログラムを構成する比較的小さな単位(ユニット)が個々の機能を正しく果たしているかどうかを検証するテストです。 通常、関数やメソッドが単体テストの単位(ユニット)となります。
なぜテストを書くのか&費用対効果の話
- 主に反復型開発において、既存のメソッドの挙動を担保しつつ、新しい機能を盛り込んでいく過程でバグの発見することが容易になる
- よく書かれたテストはそれだけでクラスの取り扱い説明書になる
事情は多々あると思いますが、ぱっと見で単体テストを書くコストが高くつくからです。(ついているように見えるだけですが)
このコストをどう捉えるか?単体テストにどのような意味をもたせるかは会社やプロジェクトの方針に依るところが大きいと思います。
私の私見では、部分的でも8割位のプロジェクトでは書いたほうがよいと思いますが、2割位は書いても意味のない状況というのは確実に存在するので、一度は対コストのことを考えてもよいと思います。
単体テストを残すかどうか、どの程度残すかは、プロダクトの品質と、コストの話なので、スクラムでいうところのプロダクトオーナーが判断すべきか、開発チームの自治としてスクラムメンバーが判断するか、きっとお話し合いだろうなー、と思います。
Javaでいうところの単体テスト
実装したクラスが正しく動作するかを裏打ちするための実装のこと。
古くから JUnit という仕組みが用いられており、クラス単体ではなくクラスの複合的なテストも書くことができる、が、それを単体テストと呼ぶかどうかは知らない。(連結テスト、インテグレーションテストっぽいなにかだと思う)
単体テストを書くコストは、初級者で被テストクラスを実装した時間的なコストで3倍、なれてくると同時にテストを書くようになるので等倍近くなります。
時間的なコストで1.5倍を切ったらまあ合格にしてあげてもよくってよ。
単体テストが「たんたい、Unit」と呼ばれる理由
最小の集合あるいは機能の単位であるクラスについてテストを行うからです。
このテストを行うための決まり事がいくつかあります。
- 他のクラスの挙動にテスト結果が左右されない
- カバレッジが100%である(必要はないと個人的には思うけどそれなりに高い)こと
- 値範囲に網羅性があること
具体的に何書くんじゃい
では、今回 Spring Boot をUTを例にしてみます。
おおよそ単体としては3種類のテストがあり、それぞれのテストをサポートするツールやライブラリがあります。
(依頼主から答えは見つけさせてと言われてるので、何を使うかは言わないことにします)
- Controller
- Service
- Repository
Controller
Controller はリクエスト及びレスポンスの処理について責任を追うもので、リクエストのバリデーションのバリエーションやレスポンスがきちんと機能しているかを見るものです。
そのため、Spring Bootというフレームワークの特性も含めテストします。
厳密には「単体」ではないのですが、単体テストに含めて運用したほうが効率がよいです。
これを実現するためには、コントローラーだけをテストできる仕組みが必要になります。
Service
主に「書いてあること」が仕様どおりであるかをテストします。
詳しくは後述。
Repository
レポジトリがたとえばDBに書き込まれたり読み出せるかをテストするものです。
厳密にはデータベースなどと連携した挙動をテストするため、「単体」ではないのですが、単体テストに含めて運用したほうが効率がよいです。
これを実行するためにはテストの中で完結するインスタントなDBが必要になります。
Serviceについてちょっと深堀り
こんなかんじのクラスがあったとしましょう。
@RequiredArgsConstructor
@Service
public class SomeService {
private final RepositoryA repositoryA;
private final RepositoryB repositoryB;
private final UtilServiceA utilServiceA;
private final AutoOrderRepository autoOrderRepository;
public void savePrice(int id, int price, int length) {
DtoObject dtoObject = new DtoObject();
dtoObject.setId(id);
dtoObject.setPrice(price);
dtoObject.setLength(length);
repositoryA.insert(dtoObject);
}
}
savePrice
というメソッドの責任は、レポジトリAのPK idに対し、priceとlengthをレポジトリに渡すこと
になります。
これをテストする場合、はたしてどうすべきでしょうか。
(つづく)