かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot 2.0.x の Web アプリを 2.1.x へバージョンアップする ( その13 )( @WebMvcTest、@WithAnonymousUser、@WithMockUser を使ってみる )

概要

記事一覧はこちらです。

Spring Boot 2.0.x の Web アプリを 2.1.x へバージョンアップする ( その12 )( @Rule を使用しているテストを JUnit 5 のテストに書き直す2 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • @WebMvcTest アノテーションは Spring Boot 1.4 の時に作成されたものですが、これまで使用していなかったので使用したテストを作成してみます。
    • 一緒に @WithAnonymousUser、@WithMockUser アノテーションも使用します。
    • JUnit 4 や Spock ではなく JUnit 5 のテストを作成します。

参照したサイト・書籍

  1. Spring Boot Reference Guide - 46.3.10 Auto-configured Spring MVC Tests
    https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/reference/htmlsingle/#boot-features-testing-spring-boot-applications-testing-autoconfigured-mvc-tests

  2. Spring Boot Reference Guide - Appendix D. Test auto-configuration annotations
    https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/reference/htmlsingle/#test-auto-configuration

  3. Spring Security 使い方メモ テスト
    https://qiita.com/opengl-8080/items/eaa8f4eb9286a3df7986#%E3%82%88%E3%81%8F%E4%BD%BF%E3%81%86%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E5%AE%9A%E7%BE%A9%E3%82%92%E3%81%BE%E3%81%A8%E3%82%81%E3%82%8B

目次

  1. @WebMvcTest、@WithAnonymousUser、@WithMockUser を使ってテストを作成する。。。が、@WebMvcTest があるとテストが動作しない?
  2. @WebMvcTest → @SpringBootTest+@AutoConfigureMockMvc に変更する
  3. @WithMockUser には username 属性を指定すべきか? roles 属性を指定すべきか?
  4. まとめ

手順

@WebMvcTest、@WithAnonymousUser、@WithMockUser を使ってテストを作成する。。。が、@WebMvcTest があるとテストが動作しない?

src/main/java/ksbysample/webapp/lending/web/admin/library/AdminLibraryController.java はクラスに @PreAuthorize("hasRole('ROLE_ADMIN')") を付与して ROLE_ADMIN でないとアクセスできないようにしています。

@Controller
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping("/admin/library")
public class AdminLibraryController {

    private final AdminLibraryService adminLibraryService;

    /**
     * @param adminLibraryService ???
     */
    public AdminLibraryController(AdminLibraryService adminLibraryService) {
        this.adminLibraryService = adminLibraryService;
    }

    /**
     * @return ???
     */
    @RequestMapping
    public String index() {
        return "admin/library/library";
    }

    /**
     * @param setSelectedLibraryForm ???
     * @return ???
     */
    @RequestMapping("/addSearchLibrary")
    public String addSearchLibrary(SetSelectedLibraryForm setSelectedLibraryForm) {
        adminLibraryService.deleteAndInsertLibraryForSearch(setSelectedLibraryForm);
        return "redirect:" + Constant.URL_ADMIN_LIBRARY;
    }

}

src/test/java/ksbysample/webapp/lending/web/admin/library/AdminLibraryControllerTest.java では @SpringBootTest を付与して SecurityMockMvcExtension クラスで MockMvc を自分で初期化してテストしていますが、@WebMvcTest、@WithAnonymousUser、@WithMockUser を使ってテストを作成してみます。

src/test/java/ksbysample/webapp/lending/web/admin/library/AdminLibraryControllerTestWithMockUser.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.lending.web.admin.library;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(AdminLibraryController.class)
class AdminLibraryControllerTestWithMockUser {

    @Autowired
    private MockMvc mvc;

    // AdminLibraryController 内で DI している AdminLibraryService は mock にする
    @MockBean
    private AdminLibraryService adminLibraryService;

    @WithAnonymousUser
    @Test
    void 認証していなければログイン画面にリダイレクトされる() throws Exception {
        mvc.perform(get("/admin/library"))
                .andExpect(status().isFound())
                .andExpect(redirectedUrl("http://localhost/"));
    }

    @WithMockUser(roles = "USER")
    @Test
    void ROLE_ADMINがなければ403が返る() throws Exception {
        mvc.perform(get("/admin/library"))
                .andExpect(status().isForbidden());
    }

    @WithMockUser(roles = "ADMIN")
    @Test
    void ROLE_ADMINがあれば200が返る() throws Exception {
        mvc.perform(get("/admin/library"))
                .andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(model().hasNoErrors());
    }

}

このテストを実行すると全てのテストが失敗しました。。。

f:id:ksby:20190310205211p:plain

原因を確認すると、

  • エラーの原因として Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.amqp.rabbit.connection.ConnectionFactory' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {} のメッセージが出力されています。
  • それが原因で testDataExtension bean と applicationConfig bean が失敗していました。
  • applicationConfig bean で DI している connectionFactory bean を生成しているのは org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration でした。Spring Boot Reference Guide - Appendix D. Test auto-configuration annotations を見ると @WebMvcTest の時の import 対象になっていないので当然生成されません。
  • applicationConfig bean は @Configuration+@Bean アノテーションで定義していて @WebMvcTest の時も bean の生成対象になるのですが、connectionFactory bean を DI できないのでエラーになります。
  • 1. Spring Boot Reference Guide - 46.3.10 Auto-configured Spring MVC Tests には Regular @Component beans are not scanned when using this annotation. という記述があり、testDataExtension bean は @Component アノテーションを付与した bean で、かつこのテストクラスでは使用していないのですが、どうも bean の生成処理が行われているようです。中で dataSource bean を DI していますがその bean を定義している applicationConfig bean が生成されていないので、その結果として生成に失敗していました。

今の実装だと @WebMvcTest が使えませんね。。。

@SpringBootTest アノテーションを付与すると確かに初期化には時間がかかるのですが、基本的に Spring の DI コンテナに生成されたインスタンスはテスト間で使い回されるので @WebMvcTest や @RestClientTest 等を使い分ける必要はないような気がします。

今回はすぐに @WebMvcTest を使えるようにできなさそうなので @SpringBootTest に変えることにします。

@WebMvcTest → @SpringBootTest+@AutoConfigureMockMvc に変更する

src/test/java/ksbysample/webapp/lending/web/admin/library/AdminLibraryControllerTestWithMockUser.java を以下のように変更します。

package ksbysample.webapp.lending.web.admin.library;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class AdminLibraryControllerTestWithMockUser {

    @Autowired
    private MockMvc mvc;

    // AdminLibraryController 内で DI している AdminLibraryService は mock にする
    @MockBean
    private AdminLibraryService adminLibraryService;

    @WithAnonymousUser
    @Test
    void 認証していなければログイン画面にリダイレクトされる() throws Exception {
        mvc.perform(get("/admin/library"))
                .andExpect(status().isFound())
                .andExpect(redirectedUrl("http://localhost/"));
    }

    @WithMockUser(roles = "USER")
    @Test
    void ROLE_ADMINがなければ403が返る() throws Exception {
        mvc.perform(get("/admin/library"))
                .andExpect(status().isForbidden());
    }

    @WithMockUser(roles = "ADMIN")
    @Test
    void ROLE_ADMINがあれば200が返る() throws Exception {
        mvc.perform(get("/admin/library"))
                .andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(model().hasNoErrors());
    }

}
  • @WebMvcTest@SpringBootTest@AutoConfigureMockMvc に変更します。
  • @AutoConfigureMockMvc を付けて MockMvc mvc の初期化処理の記述を不要にしています。

テストを実行すると、今度は全て成功しました。

f:id:ksby:20190310212036p:plain

@WithMockUser には username 属性を指定すべきか? roles 属性を指定すべきか?

@WithMockUser には username 属性が指定できるのですが、roles 属性の代わりに username = "tanaka.taro@sample.com" を指定すると(tanaka.taro@sample.com ユーザは ROLE_ADMIN権限を持っています)、

//    @WithMockUser(roles = "ADMIN")
    @WithMockUser(username = "tanaka.taro@sample.com")
    @Test
    void ROLE_ADMINがあれば200が返る() throws Exception {
        mvc.perform(get("/admin/library"))
                .andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(model().hasNoErrors());
    }

テストは失敗しました。

f:id:ksby:20190310225226p:plain

@WithMockUser を付与すると何が行わえるのか確認してみます。org.springframework.security.test.context.support.WithMockUser を見ると @WithSecurityContext(factory = WithMockUserSecurityContextFactory.class) という記述があり、factory クラスのようなのでおそらくこれで初期化処理が行われているはずで、

f:id:ksby:20190310225526p:plain

org.springframework.security.test.context.support.WithMockUserSecurityContextFactory を見ると createSecurityContext メソッドの最後で Authentication クラスのインスタンスを生成して SecurityContext にセットしていました。username 属性を指定すると UserDetailsService クラスを使用して DB から ROLE を取得してくれるのかと思いましたが、認証処理はスキップして認証完了の状態にしているだけでした。

f:id:ksby:20190310225704p:plain

UserDetailsService クラスを使用して設定したい場合には @WithUserDetails アノテーションを使用して以下のように実装すれば、

//    @WithMockUser(roles = "ADMIN")
    @WithUserDetails("tanaka.taro@sample.com")
    @Test
    void ROLE_ADMINがあれば200が返る() throws Exception {
        mvc.perform(get("/admin/library"))
                .andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(model().hasNoErrors());
    }

テストは成功します。

f:id:ksby:20190310230354p:plain

テストの時は ROLE での動作確認が出来れば十分なので、roles 属性を指定できれば十分ですね。username 属性を指定することはまずなさそうです。

また @WithMockUser(username = "tanaka.taro@sample.com") を指定してエラーになった時に気づきましたが、いろいろ調査に必要な情報が出力されていました。MockMvc の初期化処理で何か行われているようです。MockMvc の初期化処理は下手に自分でやるよりも @AutoConfigureMockMvc に任せた方が良さそうです。

f:id:ksby:20190310231903p:plain

まとめ

  • 個人的には使うとしても @SpringBootTest+@AutoConfigureMockMvc の組み合わせの方で、@WebMvcTest を使うことはないと思います。
  • @AutoConfigureMockMvc で初期化処理した MockMvc はエラー時にいろいろな情報を出力してくれるので便利。積極的に使った方がよい。MockMvc を自分で初期化する必要はないでしょう。
  • @WithMockUser は roles 属性を指定する。username 属性を指定する必要はまずない。@WithMockUser を指定すると認証済の状態(SecurityContextHolder に Authentication クラスのインスタンスをセットした SecurityContext をセットした状態)にしてくれるので ROLE のテストをしたい場合には便利。

履歴

2019/03/10
初版発行。