【Java/Spring boot】JPAを使ったアプリケーションのHTTPテストでDBに更新が反映されない問題を解決する
こんにちは、たけちゃんです。
夏の気配を感じる今日この頃です。
今年はラニーニャ現象が発生しているらしく、暑い夏になりそうです。
京都の夏はとても暑いので今から戦々恐々としております。
ところで、最近Javaを触る機会があったのですが、結合テストを作成した際にハマったのでその体験談を投稿させて頂きます。
前提条件
・JDK21
・Spring Boot 3.2.4
失敗した結合テストの事例
まずは具体的な事例から。
以下のUserControllerに対するHTTPテストを考えます。
@Controller
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UpdateUserUseCase updateUserUseCase;
@Transactional
@PutMapping("/{id}")
public String update(
@PathVariable Long id,
@Valid @ModelAttribute("userForm") UpdateUserForm form,
BindingResult result,
RedirectAttributes redirectAttributes,
Model model
) {
if (result.hasErrors()) {
redirectAttributes.addFlashAttribute("userForm", form);
redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.userForm", result);
return edit(id, model);
}
// userエンティティを入力値で更新
Optional<User> storedUser = userRepository.findById(id);
if (storedUser.isEmpty()) {
throw new UserNotFoundException("ユーザーが見つかりません");
}
User user = storedUser.get();
user.setName(form.getName());
user.setEmail(form.getEmail());
user.setPassword(passwordEncoder.encode(form.getPassword()));
userRepository.save(user);
entityManager.flush();
redirectAttributes.addFlashAttribute("infoMessage", "管理ユーザー(id: " + id + ")を更新しました");
return "redirect:/users";
}
}
以下は/users/{userId}に対してPUTメソッドをとばした際にUserのレコードが更新されることをテストしています。
しかし、以下のテストでは最後のDBの検証が失敗してしまいます。
ログを確認したところどうやらDBの状態が更新前のままになっているようでした。
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class UserControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
@WithMockUser
public void test正常にユーザーを更新できる() throws Exception {
// 更新対象のレコードを作成
List<User> users = setUpRecords(1);
User user = users.getFirst();
// 更新内容をセット
MultiValueMap<String, String> data =
new LinkedMultiValueMap<>() {{
add("name", "鈴木次郎");
add("email", "test2@example.com");
add("password", "password");
}};
// PUTメソッドの送信
mvc.perform((put("/admin/users/" + user.getId()).params(data).with(csrf()).accept(MediaType.TEXT_HTML)));
//DBの検証
assertEquals(1, JdbcTestUtils.countRowsInTableWhere( jdbcTemplate, "users", "id = " + user.getId() + " AND name = '鈴木次郎' AND email = 'test2@example.com'" ));
}
}
原因
JPAの実装を提供しているRed Hatのリファレンスを見てみると以下の一文が。
flush()
を明示的に使用する場合を除き、エンティティマネージャが JDBC コールを実行するタイミングに関して絶対的な保証はありません。
JPAはエンティティの変更をメモリ内で管理し、トランザクションがコミットされるまで実際のDBには変更を反映しないようです。
解決策
この問題を解決するために、JPAのEntityManager
のflush()
メソッドを使用して、メモリ内の変更を即座にDBに反映させ、無事テストを通すことができました。
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class UserControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private JdbcTemplate jdbcTemplate;
@PersistenceContext
EntityManager entityManager;
@Test
@WithMockUser
public void test正常にユーザーを更新できる() throws Exception {
// 更新対象のレコードを作成
List<User> users = setUpRecords(1);
User user = users.getFirst();
// 更新内容をセット
MultiValueMap<String, String> data =
new LinkedMultiValueMap<>() {{
add("name", "鈴木次郎");
add("email", "test2@example.com");
add("password", "password");
}};
// PUTメソッドの送信
mvc.perform((put("/admin/users/" + user.getId()).params(data).with(csrf()).accept(MediaType.TEXT_HTML)));
// DBの状態を同期
entityManager.flush();
//DBの検証
assertEquals(1, JdbcTestUtils.countRowsInTableWhere( jdbcTemplate, "users", "id = " + user.getId() + " AND name = '鈴木次郎' AND email = 'test2@example.com'" ));
}
}
まとめ
今回のようにテストメソッドでトランザクションを張っていて、実装コードのトランザクションとネストしているケースでは、
テスト側のトランザクションに対しても明示的にEntityManager.flush()を呼んであげてDBの状態を同期させる必要があるようです。
同様な点でハマっている人の参考になれば幸いです!