はじめに
所属しているチームでは、不具合があった場合、テストコードを書いて再現(レッドバーを確認)させてから修正するというルールにしています。
先日、トランザクション制御が抜けていたという不具合があり、テストコードを書こうとしたのですが、ドハマリしたので整理して投稿します。
テストコードはGithub(https://github.com/shimi58/transactiontest) に挙げているので、参考にしてください。
開発環境
Eclipse上で構築した環境は以下の通り。Gradleプロジェクトを作成してパスを通しています。
環境 | バージョン |
---|---|
Java | 1.8 |
SpringBoot | 2.2.6 |
MyBatis | 2.1.0 |
H2 | PostgresqlMode |
Junit | 5(Jupiter) |
実装コード(サービスクラス)
従業員情報(名前、電話番号、メールアドレス)を登録して、従業員テーブルの件数をステータステーブルに更新する。といったサンプルソースです。
/**
* 従業員サービス
*/
@Service
public class EmployeeService {
@Autowired
EmployeeRepository employeeRepository;
/**
* 従業員登録
*/
@Transactional //←これが漏れていた
public EmployeeNumber register(Employee employee) {
employeeRepository.registerEmployee(employee);
Employees employees = employeeRepository.findEmployees();
EmployeeNumber employeeNumber = employees.number();
employeeRepository.registerNumber(employeeNumber);
return employeeNumber;
}
}
今回のサンプルソースでは、従業員テーブル→ステータステーブルと更新する訳ですが、トランザクションが効いていないと、ステータステーブルの更新に失敗した場合、従業員テーブルで保持している件数とステータステーブルに保持している件数が不一致になるわけですね。
ちなみに、Controllerクラスについても後々のテストコードの説明のときに出てくるので記載しておきます。
/**
* 従業員Controller
*/
@RestController
@RequestMapping("/employees")
public class EmployeesController {
@Autowired
EmployeeService employeeService;
/**
* 従業員登録
*/
@RequestMapping(value = "/register", method = {RequestMethod.POST})
public String regist(@RequestBody Employee employee) {
EmployeeNumber employeeNumber = new EmployeeNumber(0);
try {
employeeNumber = employeeService.register(employee);
} catch (Exception e) {
System.out.println("処理エラー");
}
return employeeNumber.toString();
}
}
どのようにテストをするか
@SpyBeanを使います。
SpyBeanとはSpring Bootの機能で、テストしたいクラスの一部処理だけをMock化してくれるスグレモノです。
今回のサンプルソース場合、EmployeeRepositoryをSpyBeanでMock化して、
- registerEmployee:従業員登録処理→通常実施
- findEmployees:従業員テーブルカウント処理→通常実施
- registerNumber:ステータステーブル更新処理→エラー発生
という状況を作り出したいです。
テストコード
テストは、SpringBootTestとしてControllerクラスを呼び出して実施する形になります。
Controllerクラスが上記のサービスクラスを呼び出しています。
@SpringBootTest
public class EmployeesControllerTransactionTest {
MockMvc mockMvc;
@Autowired
private ObjectMapper mapper;
@Autowired
private EmployeesController employeesController;
// Mock対象
@SpyBean
private EmployeeRepository employeeRepository;
/**
* Transactionテスト
*
* Emploeeテーブルに従業員情報を登録後、従業員件数更新時にエラーが発生した際、 <br>
* 従業員情報登録をロールバックしていることを確認する
*
* @throws Exception
*/
@ParameterizedTest
@CsvSource({"日比 隆夫,123-4345-2352,hibi@test.com,3"})
public void testTransaction(String name, String phone, String mail, int expected)
throws Exception {
Employee employee = new Employee(name, phone, mail);
// リクエストをJson形式に変換
String json = mapper.writeValueAsString(employee);
// エラー発生設定
doThrow(new RuntimeException()).when(employeeRepository)
.registerNumber(Mockito.any(EmployeeNumber.class));
this.mockMvc = MockMvcBuilders.standaloneSetup(employeesController).build();
// リクエスト発行
MvcResult result = mockMvc.perform(
post("/employees/register").contentType(MediaType.APPLICATION_JSON).content(json))
.andExpect(status().isOk()).andReturn();
String response = result.getResponse().getContentAsString();
Employees employees = employeeRepository.findEmployees();
System.out.println(response);
System.out.println(employees.toString());
EmployeeNumber actual = employees.number();
// ロールバックされるため、登録前の3件の状態になっていることを確認
assertEquals(expected, actual.getValue().intValue());
}
テスト実施のポイント
@SpringBootTest
public class EmployeesControllerTransactionTest {
SpringBootTestをつけないとDIしてくれないので必須ですね。
@SpyBean
private EmployeeRepository employeeRepository;
Mock対象のRepositoryを宣言しておきます。
@ParameterizedTest
@CsvSource({"日比 隆夫,123-4345-2352,hibi@test.com,3"})
本稿とは関係ないですが、CsvSourceを使うことで、1メソッドでバリエーションテストを行う事ができます。
Junit4歴が長かったのでこれができると分かったときは感動しました。
ちなみに、名前はすごい名前生成器から拝借しました。便利な世の中になりましたねぇ。(もっと関係ない。)
// エラー発生設定
doThrow(new RuntimeException()).when(employeeRepository)
.registerNumber(Mockito.any(EmployeeNumber.class));
ここで、registerNumber処理時にRuntimeExceptionを発生させますよーと宣言しています。
this.mockMvc = MockMvcBuilders.standaloneSetup(employeesController).build();
ドハマリポイント①。standaloneSetupさせとかないと、mockMvcがヌルポで落ちます。
MvcResult result = mockMvc.perform(
post("/employees/register").contentType(MediaType.APPLICATION_JSON).content(json))
.andExpect(status().isOk()).andReturn();
ドハマリポイント②。status().isOk()じゃないときは彼方に飛んでいきます(デバックが返ってこない)。postのURL間違っていて、404返ってきてたときとか、なんでデバック反応しないんだーって小一時間悩んでました。
テスト実施結果
テストコードを実行してみると、見事グリーンバーになってくれました!
ちなみに、サービスクラスの@Transactionalを外すと、、
うん、レッドバーですね。従業員テーブルの件数が1件違うよとアサーションエラーとなっており、トランザクション確認できてます。
最後に
一人ふりかえりしてみます。
分かったこと
- H2のインメモリーDBがめちゃ便利
今回、初めて触ったのですが、こういったサンプルコードの動作確認にもってこいですね。
A5SQLとかで接続できればもっと完璧ですねぇ。 - Gradleがめちゃ便利
定義書くだけでパスを通してくれるので、すごく捗ります。
余談ですが、依存を辿って結構な数のライブラリを落としてくることになるので、ライセンスの確認が品証泣かせになっています(笑) - SpyBeanがめちゃ便利
Mock使うとテストできないところはないのではないかと。 - 1から処理を書くのは大変
Gradleとかpostの受け取り方とか、テストコードとか業務で書いたはずなのに、1から書くのは地味に時間かかりました。。(けど実装は楽しいなぁ) - H2のPostgresModeはUpsertができない
みたいですH2 Database Postgres Mode Upsert。
そのため、初期構築データでステータステーブルにレコードを登録しておくようにしておき、updateだけで済むようにしておきました。(イケてない。。)
次やること
- テスト道を極めるためにテスト駆動設計を読み切って整理する
- 他のMock処理が動くように色んなバリエーションのテストコードをサンプルソースとして動くようにしておく