LoginSignup
25
28

More than 5 years have passed since last update.

Spring Bootで認証を通さずにWebAPIの単体テストを行う方法

Posted at

■環境

Spring Boot 1.2.5.Release
Spring Security 4.0.1.Release
Java 8
Maven 3.3.1

■概要

Spring Bootで認証を行っていると、単体テスト時に認証を通さないための工夫が必要になります。
その手順を解説します。

■pom.xml

今回説明する範囲では不要なものもあるかと思いますが・・・

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
<!-- ■Spring Boot本体 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

<!-- ■Spring Boot関連 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!-- ■テスト関連 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <version>4.0.1.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
             <groupId>com.jayway.jsonpath</groupId>
            <artifactId>json-path</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.skyscreamer</groupId>
            <artifactId>jsonassert</artifactId>
            <version>1.2.3</version>
            <scope>test</scope>
        </dependency>

<!-- ■Javaライブラリ -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.4</version>
            <scope>provided</scope>
        </dependency>

<!-- ■フロントエンドフレームワーク -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>3.3.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

■詳細

まずは単体テストを行うクラスからです。

AppsRestControllerTest.java
package com.sample.apps.controller;

import com.sample.SampleApplication;
import com.sample.common.test.controller.AbstractRestControllerTest;
import com.sample.common.test.data.AppsData;
import com.sample.apps.service.AppsService;

import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

/**
 * AppsRestControllerのテストクラス
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleApplication.class)
@WebAppConfiguration
public class AppsRestControllerTest extends AbstractRestControllerTest {

    @Mock
    private AppsService appsService;

    @InjectMocks
    @Autowired
    private AppsRestController appsRestController;

    @Test
    public void test_DataAppsDetail_OK() {
        try {
            long keyId = AppsData.getAppsData().getKeyId();
            // サービス層のモック
            Mockito.doReturn(AppsData.getAppsData()).when(appsService).findByKeyId(Mockito.anyLong());

            // 認証済みにする
            mvc.setAuthentication();
            // WebAPIを実行
            mvc.mockMvc.perform(MockMvcRequestBuilders
                    .get("/data/apps/detail/{keyId}", keyId)
                    )
                // 認証されている
                .andExpect(SecurityMockMvcResultMatchers.authenticated())
                // HTTPステータスがOK(200)
                .andExpect(MockMvcResultMatchers.status().isOk())
                // contentTypeの評価
                .andExpect(MockMvcResultMatchers
                        .content().contentType("application/json;charset=UTF-8"))
                // content内容の評価(contentのサイズが0ではない)
                .andExpect(MockMvcResultMatchers.jsonPath(
                        "$", CoreMatchers.not(Matchers.hasSize(0))))
                // content内容の評価(仕様で定められたカラムが存在するか)
                .andExpect(MockMvcResultMatchers
                        .jsonPath("$.keyId").exists())
                .andExpect(MockMvcResultMatchers
                        .jsonPath("$.keyName").exists())
                // request、responseを出力
                .andDo(MockMvcResultHandlers.print());

            Mockito.verify(appsService).findByKeyId(Mockito.anyLong());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ポイントは以下の部分です。

            // 認証済みにする
            mvc.setAuthentication();

認証済みにさせることにより、認証が必要なWebAPIの単体テストを行うことができます。
詳細を見ていきます。
まずはRestControllerのテストを行う抽象クラス

AbstractRestControllerTest.java
package com.sample.common.test.controller;

import com.sample.SampleApplication;
import com.sample.common.test.rules.MockMvcResource;

import org.junit.Rule;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

/**
 * RestControllerのテストを行う抽象クラス
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleApplication.class)
@WebAppConfiguration
public abstract class AbstractRestControllerTest {

    @Rule
    @Autowired
    public MockMvcResource mvc;

    @Rule
    public MockitoRule rule = MockitoJUnit.rule();
}

MockitoRuleは、コントローラのフィールドにモックを仕込みたい場合に用意します。

MockMvcResourceは自前で作成したクラスです。
テストクラス共通で使用する内容を定義しました。

MockMvcResource.java
package com.sample.common.test.rules;

import javax.servlet.Filter;

import com.sample.common.test.data.LoginUserData;

import org.junit.rules.ExternalResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.stereotype.Component;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

/**
 * テストクラス共通で使用する内容を定義する
 */
@Component
public class MockMvcResource extends ExternalResource {

    @Autowired
    private WebApplicationContext webAppContext;

    private SecurityContext securityContext;

    @Autowired
    private Filter springSecurityFilterChain;

    public MockMvc mockMvc;

    @Override
    protected void before() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();
    }

    @Override
    protected void after() {
        SecurityContextHolder.clearContext();
    }

    /**
     * 認証済みにさせる
     */
    public void setAuthentication(){
        Authentication authentication =
                new UsernamePasswordAuthenticationToken(
                        LoginUserData.getLoginUserData(),
                        LoginUserData.getLoginUserData().getPassword(),
                        AuthorityUtils.createAuthorityList("ROLE_USER"));
        securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(authentication);
        SecurityContextHolder.setContext(securityContext);
    }
}

以下が認証済みにさせている部分です。

    /**
     * 認証済みにさせる
     */
    public void setAuthentication(){
        Authentication authentication =
                new UsernamePasswordAuthenticationToken(
                        LoginUserData.getLoginUserData(),
                        LoginUserData.getLoginUserData().getPassword(),
                        AuthorityUtils.createAuthorityList("ROLE_USER"));
        securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(authentication);
        SecurityContextHolder.setContext(securityContext);
    }

UsernamePasswordAuthenticationTokenクラスに認証させるユーザー情報を渡して、それを

        securityContext.setAuthentication(authentication);
        SecurityContextHolder.setContext(securityContext);

としてSecurityContextHolderにセットしてあげると認証済みになります。
ここで出てきたUsernamePasswordAuthenticationTokenやSecurityContextHolderなどはすべてSpring Securityのクラスです。
Spring Securityのクラスを駆使することで認証済みにできるわけです。

また、この際渡すユーザー情報は何でもよいです。
ユーザー情報はテストクラス共通で使いまわせるように別クラスにしました。

LoginUserData.java
package com.sample.common.test.data;

import com.sample.db.domain.entity.loginuser.custom.MLoginUser;
import com.sample.login.service.data.LoginUser;

/**
 * テストクラスで使用する認証ユーザーの情報
 */
public class LoginUserData {

    private static final MLoginUser mLoginUser = new MLoginUser(
            "user1", // String loginUserId;
            "user1" // String password;
            );

    private static final LoginUser loginUser = new LoginUser(mLoginUser);

    public static LoginUser getLoginUserData(){
        return loginUser;
    }
}

before、afterメソッドでテストクラス共有の前処理、後処理を定義しています。

    @Override
    protected void before() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();
    }

    @Override
    protected void after() {
        SecurityContextHolder.clearContext();
    }

■まとめ

・認証済みにさせることにより、認証が必要なWebAPIの単体テストを行うことができます
・Spring Securityのクラスを使うことで認証済みにすることができます
・認証情報は適当でOK

■参考資料

From 0 to Spring Security 4.0
Spring Bootでコントローラの単体テスト

25
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
28