■環境
Spring Boot 1.2.5.Release
Spring Security 4.0.1.Release
Java 8
Maven 3.3.1
■概要
Spring Bootで認証を行っていると、単体テスト時に認証を通さないための工夫が必要になります。
その手順を解説します。
■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>
■詳細
まずは単体テストを行うクラスからです。
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のテストを行う抽象クラス
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は自前で作成したクラスです。
テストクラス共通で使用する内容を定義しました。
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のクラスを駆使することで認証済みにできるわけです。
また、この際渡すユーザー情報は何でもよいです。
ユーザー情報はテストクラス共通で使いまわせるように別クラスにしました。
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