開発ブログ

株式会社Nextatのスタッフがお送りする技術コラムメインのブログ。

電話でのお問合わせ 075-744-6842 ([月]-[金] 10:00〜17:00)

  1. top >
  2. 開発ブログ >
  3. Java >
  4. 【Java/Spring boot】JPAを使ったアプリケーションのHTTPテストでDBに更新が反映されない問題を解決する
no-image

【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のEntityManagerflush()メソッドを使用して、メモリ内の変更を即座に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の状態を同期させる必要があるようです。
同様な点でハマっている人の参考になれば幸いです!

  • posted by たけちゃん
  • Java
TOPに戻る