Spring Security ユニットテスト

概要

Spring Securityを使用する場合、ユニットテスト(単体テスト)をする方法について調べて見ました。

※本記事で紹介する内容はSpring Security 4.1から追加されました。

既存のテスト方法

Spring SecurityはSecurityContextを通して、現在実行しているスレッドと関連している認証情報を管理します。したがって、テスト実行時にSecurityContextHolderを介して認証情報を設定すると、ログインした状態などの認証に関するテストができます。

class SomeTest {
    //...
    @Before
    public void setUp() {
        SecurityContextHolder.getContext()
            .setAuthentication(new UsernamePasswordAuthenticationToken("username", "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))));
    }
    //...
}

※ 上記のようにユニットテストを実行する前に認証情報を設定すると、ログインした状態でテストが可能です。しかし、上記のようなコードは、やはりSpringらしくないですね。

新しく追加された機能

Spring Security 4.1からアノテーション(Annotation)を通した宣言的なテスティングフィチャーが追加されました。

アノテーション 説明
@WithAnonymousUser 匿名ユーザの認証情報を設定するためのアノテーション
@WithUserDetails UserDetailsServiceからユーザー情報を取得して認証情報を設定するためのアノテーション(4.0から追加され、4.1からはSpring Bean名も指定できるようになりました)
@WithMockUser 別のUserDetailsServiceのスタブを作成しなくても簡単に認証情報の設定ができるアノテーション(4.0から追加、4.1から動作する)

使い方

設定

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    runtimeOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

まず、GradleなどでSpring Securityに関する依存性を設定する必要があります。テストスコープでorg.springframework.security:spring-security-testモジュールを追加します。

@WithAnonymousUser

認証されていない状態を設定するためのアノテーションです。認証していない場合を示すアノテーションがなぜ必要かと思われますが、前述のようにSpring Securityは、現在実行中のスレッドに関連づけて、認証情報を管理するため、一度の認証情報を設定してテストを行った場合に、認証状態を初期化させる必要があります。そうしないと次のテストで以前のテストの認証情報がそのまま使われてしまいます。

class SomeTest {

    //...
    @Test
    @WithAnonymousUser
    public void some_test() {
        ...
    }
    //...

}

@WithUserDetails

Spring Securityでユーザーの情報を照会するために使われるUserDetailsServiceを使って認証情報を設定するアノテーションです。

class SomeTestConfiguration {
    //...
    @Bean
    @Profile("test")
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                return User
                        .withUsername(username)
                        .password("password")
                        .authorities(new SimpleGrantedAuthority("ROLE_USER"))
                        .build();
            }
        };
    }
    //...
}
class SomeTest {
   //...
    @Test
    @WithUserDetails("test_user")
    public void some_test() {
        ...
    }
    //...
}

上記のようにテスト用のUserDetailsServiceのBeanを登録して使います。

@WithMockUser

単にusernameとroleくらいの記述するこてで、認証情報を設定してくれるアノテーションです。別の設定がいらないので簡単に使えます。

class SomeTest {
    //...
    @Test
    @WithMockUser(username = "username", roles = "USER")
    public void some_test() {
        ...
    }
    //...
}

カスタムアノテーション

ほとんどの場合、上のコードで紹介したアノテーション(Annotation)を使用すると、テストが可能になると思われるが、認証主体(AuthenticationPrincipal)に関する情報をUserDetailsを使用していない場合には、別のカスタムアノテーションを作成することができます。

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithCustomMockUser {

    String userNo() default "1";

    String userId() default "user";

    String name() default "name";

}
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithCustomMockUser> {
    @Override
    public SecurityContext createSecurityContext(WithCustomMockUser user) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        CustomUser principal = new CustomUser(Long.valueOf(user.memberNo()), user.userId(), user.name());
        Authentication auth = new UsernamePasswordAuthenticationToken(principal, "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_MEMBER")));
        context.setAuthentication(auth);
        return context;
    }
}

上記のようにカスタムアノテーションを作成し、他のテストアノテーションと同様に使用すると、カスタム認証情報が設定された状態でテストができます。

class SomeTest {
    //...
    @Test
    @WithCustomMockUser
    public void some_test() {
        //...
    }
    //...
}

合成アノテーション(Composition Annotation)

また、合成アノテーションを作成し、繰り返されるコードを省略することができます。

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="admin",roles="ADMIN")
public @interface WithCustomMockAdmin {
}
class SomeTest {
    //...
    @Test
    @WithCustomMockAdmin
    public void some_test() {
        ...
    }
    //...
}

まとめ

今まではログインなどの認証状態をテストする場合、面倒なコードを書かなければなりませんでしたが、Spring Securityで提供するテスティングフィチャーを使用することで、簡単にテストすることができますので、より安全なアプリケーションを作成することができます。

参考

Spring Security Reference