Webアプリ作成 Spring Boot
⑩テストコード Controllerクラス

今回はControllerクラスのテストコードを書いていきます。
JUnitより「Spring Test」が提供する「MockMvc」や検証するためのメソッドがメインとなります。

概要

JUnitとMockMvcを利用してテストコードを書いていきます。
MockMvcとは、Spring MVC フレームワークの動作を再現することができて、Controllerテストの際に使われたりするものです。

ServiceクラスのテストではJUnitの「assert~」の形で期待値と実行結果を検証していました。
今回はSpring Testの機能を利用して検証します。
検証内容は主に以下の点です。
・実行結果のレスポンスが成功している
・Formのバリデーション
・model値の検証
・返却値の検証

テストコード

「src/test/java/diary」に「DiayControllerTest.java」を作成します。
まずはdiaryListメソッドに対するテストコードのみ記載します。

package diary;

import static org.hamcrest.Matchers.hasSize;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

import java.util.Date;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

import diary.controller.DiaryController;
import diary.form.GetForm;
import diary.form.PostForm;
import diary.form.PutForm;

@SpringBootTest
@ActiveProfiles("unit")
public class DiaryControllerTest {

	private MockMvc mockMvc;
	
	@Autowired
    private FilterChainProxy springSecurityFilterChain;
	
	@Autowired
	DiaryController target;
	
	@BeforeEach
	public void setup() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setSuffix(".html");
		mockMvc = MockMvcBuilders.standaloneSetup(target).apply(springSecurity(springSecurityFilterChain)).setViewResolvers(viewResolver).build();
	}
	
	@Nested
	@Sql("/data-unit.sql")
	class diaryList {

		@Test
		@WithMockUser
		public void 日記一覧表示() throws Exception {
			// パラメータ設定
			GetForm form = new GetForm();

			// 実行
		    mockMvc.perform(get("/diary").flashAttr("getForm", form))
		    // 検証
		    // ステータスが正常
		    .andExpect(status().isOk())
		    // modelの検証
		    // formの値
		    .andExpect(model().attribute("getForm", form))
		    // listのサイズ
		    .andExpect(model().attribute("list", hasSize(3)))
		    // modelにエラーがないこと
		    .andExpect(model().hasNoErrors())
		    // viewの返却
		    .andExpect(view().name("list"));
		}
		
		@Test
		public void 権限なし() throws Exception {
			// when
		    mockMvc.perform(get("/diary"))
		    .andExpect(status().is3xxRedirection());
		}
	}
}
ポイント

MockMvcBuilders.standaloneSetup〜
 MockMvcを利用してテストをするために、必要な設定となります。
 利用するためには2種類の設定からテストに合わせて選択します。

設定説明
webAppContextSetupSpringMVCの設定を読み込み、本番で利用する設定とほぼ同じ内容でテストすることが可能です。
設定ファイルを読み込みテストするため設定ファイルの検証もできます
そのため、テスト対象のクラスをインスタンス生成しなくてもテストできます。
standaloneSetup特定のテスト対象クラスをインスタンス生成してテストをすることが可能です。
設定ファイルを読み込んでテストするわけではないのでよりControllerクラス単体のテストとなります。

apply(springSecurity(springSecurityFilterChain))
 SpringSecurityの機能を有効にしています。

setViewResolvers(viewResolver).build();
 この設定がないと「POST」時にViewを返却する場合にエラーとなってテストできませんでした。
 (設定がいけないのだろうか。。。)

@WithMockUser
 特定のユーザでテストを実行することが可能です。
 これを指定することで認証されたユーザとしてテストすることができます。
 また、引数にユーザ名、パスワード、権限も指定してテストすることもできます。

検証で利用したメソッドを下記にまとめます。
(この後で記載するメソッドも含む)

メソッド説明
perform擬似的なリクエストを行う
flashAttrflashAttr(パラメータ名, オブジェクト)でパラメータを設定
paramparam(パラメータ名,値)でパラメータを設定
.with(csrf()))有効な CSRF トークンをリクエストに含める
POSTメソッド時に必要でした
andExpect引数に「ResultMatcher」メソッドを指定して検証
statusHTTPステータスコードを検証
isOkHTTPステータスコードが200であることを検証
is3xxRedirectionHTTPステータスコードが300番台であることを検証
modelSpring MVCのModelを検証
attribute引数に指定したModelの値を検証
hasErrorsModelにエラーがあることを検証
hasNoErrorsModelにエラーがないことを検証
sizeModelの数を数を検証
attributeExists引数に指定したModelが存在することを検証
attributeDoesNotExist引数に指定したModelが存在しないことを検証
viewControllerが返却したViewを検証
name引数で指定したViewが返却していることを検証
redirectedUrlリダイレクト先URL検証
hasSizemodel().attribute("list", hasSize(3))
で指定したModelのsizeを検証。
org.hamcrest.Matchers.hasSizeを利用

その他のメソッドも記載します。

package diary;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import java.util.Date;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import diary.controller.DiaryController;
import diary.form.GetForm;
import diary.form.PostForm;
import diary.form.PutForm;
@SpringBootTest
@ActiveProfiles("unit")
public class DiaryControllerTest {
private MockMvc mockMvc;
@Autowired
private FilterChainProxy springSecurityFilterChain;
@Autowired
DiaryController target;
@BeforeEach
public void setup() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setSuffix(".html");
mockMvc = MockMvcBuilders.standaloneSetup(target).apply(springSecurity(springSecurityFilterChain)).setViewResolvers(viewResolver).build();
}
@Nested
@Sql("/data-unit.sql")
class diaryList {
@Test
@WithMockUser
public void 日記一覧表示() throws Exception {
// パラメータ設定
GetForm form = new GetForm();
// 実行
mockMvc.perform(get("/diary").flashAttr("getForm", form))
// 検証
// ステータスが正常
.andExpect(status().isOk())
// modelの検証
// formの値
.andExpect(model().attribute("getForm", form))
// listのサイズ
.andExpect(model().attribute("list", hasSize(3)))
// modelにエラーがないこと
.andExpect(model().hasNoErrors())
// viewの返却
.andExpect(view().name("list"));
}
@Test
public void 権限なし() throws Exception {
// 実行
mockMvc.perform(get("/diary"))
.andExpect(status().is3xxRedirection());
}
}
@Nested
@Sql("/data-unit.sql")
class showUpdate {
@Test
@WithMockUser
public void 詳細画面表示_データあり() throws Exception {
// パラメータ設定
int id = 1;
// 実行
mockMvc.perform(get("/diary/{id}", id))
// 検証
// ステータスが正常
.andExpect(status().isOk())
// modelの検証
// diaryが存在すること
.andExpect(model().attributeExists("diary"))
// errorが存在しないこと
.andExpect(model().attributeDoesNotExist("error"))
// viewの返却
.andExpect(view().name("detail"));
}
@Test
@WithMockUser
public void 詳細画面表示_データなし() throws Exception {
// パラメータ設定
int id = 99;
// 実行
mockMvc.perform(get("/diary/{id}", id))
// 検証
// ステータスが正常
.andExpect(status().isOk())
// modelの検証
// diaryが存在しないこと
.andExpect(model().attributeDoesNotExist("diary"))
// errorが存在すること
.andExpect(model().attributeExists("error"))
// viewの返却
.andExpect(view().name("detail"));
}
@Test
public void 権限なし() throws Exception {
// パラメータ設定
int id = 1;
// 実行
mockMvc.perform(get("/diary/{id}", id))
.andExpect(status().is3xxRedirection());
}
}
@Nested
@Sql("/data-unit.sql")
class formPage {
@Test
@WithMockUser
public void フォーム画面遷移_新規登録() throws Exception {
// パラメータ設定
PutForm form = new PutForm();
form.setUpdateFlag(false);			
// 実行
mockMvc.perform(post("/diary/form")
.flashAttr("putForm", form)
.with(csrf()))
// 検証
// ステータスが正常
.andExpect(status().isOk())
// modelの検証
.andExpect(model().attribute("putForm", form))
.andExpect(model().attribute("update", false))
// viewの返却
.andExpect(view().name("form"));
}
@Test
@WithMockUser
public void フォーム画面遷移_編集() throws Exception {
// パラメータ設定
PutForm form = new PutForm();
form.setUpdateFlag(true);			
// 実行
mockMvc.perform(post("/diary/form")
.flashAttr("putForm", form)
.with(csrf()))
// 検証
// ステータスが正常
.andExpect(status().isOk())
// modelの検証
.andExpect(model().attribute("putForm", form))
.andExpect(model().attribute("update", true))
// viewの返却
.andExpect(view().name("form"));
}
@Test
public void 権限なし() throws Exception {
// パラメータ設定
PutForm form = new PutForm();
form.setUpdateFlag(true);			
// 実行
mockMvc.perform(post("/diary/form")
.flashAttr("putForm", form)
.with(csrf()))
// 検証
.andExpect(status().is3xxRedirection());
}
}
@Nested
class backPage {
@Test
@WithMockUser
public void 一覧画面へ戻る_新規登録時() throws Exception {
// 実行
mockMvc.perform(post("/diary/insert")
.param("back", "back")
.with(csrf()))
// 検証
// modelの検証
// modelが存在しないこと
.andExpect(model().size(0))
// リダイレクトすること
.andExpect(redirectedUrl("/diary"));
}
@Test
@WithMockUser
public void 一覧画面へ戻る_更新時() throws Exception {
// 実行
mockMvc.perform(post("/diary/update")
.param("back", "back")
.with(csrf()))
// 検証
// modelの検証
// modelが存在しないこと
.andExpect(model().size(0))
// リダイレクトすること
.andExpect(redirectedUrl("/diary"));
}
@Test
@WithMockUser
public void 一覧画面へ戻る_フォーム画面() throws Exception {
// 実行
mockMvc.perform(post("/diary/form")
.param("back", "back")
.with(csrf()))
// 検証
// modelの検証
// modelが存在しないこと
.andExpect(model().size(0))
// リダイレクトすること
.andExpect(redirectedUrl("/diary"));
}
@Test
public void 権限なし() throws Exception {
// パラメータ設定
PutForm form = new PutForm();
form.setUpdateFlag(true);			
// 実行
mockMvc.perform(post("/diary/insert")
.param("back", "back")
.with(csrf()))
// 検証
.andExpect(status().is3xxRedirection());
}
}
@Nested
@Sql("/data-unit.sql")
class insert {
@Test
@WithMockUser
public void 登録() throws Exception {
// パラメータ設定
PostForm form = new PostForm();
form.setCategoryForm("1");
form.setTitleForm("題名");
form.setContentForm("概要");
form.setDateForm(new Date());
// 実行
mockMvc.perform(post("/diary/insert")
.param("insert", "insert")
.flashAttr("postForm", form)
.with(csrf()))	    
// 検証
// modelの検証
// フォームがエラーとならないこと
.andExpect(model().hasNoErrors())
// errorが存在しないこと
.andExpect(model().attributeDoesNotExist("error"))
// postFormの値
.andExpect(model().attribute("postForm", form))
// リダイレクトすること
.andExpect(redirectedUrl("/diary"));
}
@Test
@WithMockUser
public void パラメータエラー() throws Exception {
// パラメータ設定
PostForm form = new PostForm();
form.setCategoryForm("1");
form.setContentForm("概要");
form.setDateForm(new Date());
// 実行
mockMvc.perform(post("/diary/insert")
.param("insert", "insert")
.flashAttr("postForm", form)
.with(csrf()))	    
// 検証
// ステータスが正常
.andExpect(status().isOk())
// modelの検証
// フォームがエラーとなること
.andExpect(model().hasErrors())
// errorの値
.andExpect(model().attribute("error", "パラメータエラーが発生しました。"))
// postFormにエラーが存在すること
.andExpect(model().attributeExists("postForm"))
// viewの返却
.andExpect(view().name("form"));
}
@Test
public void 権限なし() throws Exception {
// パラメータ設定
PostForm form = new PostForm();
// 実行
mockMvc.perform(post("/diary/insert")
.param("insert", "insert")
.flashAttr("postForm", form)
.with(csrf()))
// 検証
.andExpect(status().is3xxRedirection());
}
}
@Nested
@Sql("/data-unit.sql")
class update {
@Test
@WithMockUser
public void 更新() throws Exception {
// パラメータ設定
PutForm form = new PutForm();
form.setCategoryForm("1");
form.setTitleForm("題名");
form.setContentForm("概要");
form.setDateForm(new Date());
// 実行
mockMvc.perform(post("/diary/update")
.param("update", "update")
.flashAttr("putForm", form)
.with(csrf()))	    
// 検証
// modelの検証
// フォームがエラーとならないこと
.andExpect(model().hasNoErrors())
// errorが存在しないこと
.andExpect(model().attributeDoesNotExist("error"))
// リダイレクトすること
.andExpect(redirectedUrl("/diary"));
}
@Test
@WithMockUser
public void パラメータエラー() throws Exception {
// パラメータ設定
PutForm form = new PutForm();
form.setId(1);
form.setCategoryForm("1");
form.setContentForm("概要");
form.setDateForm(new Date());
// 実行
mockMvc.perform(post("/diary/update")
.param("update", "update")
.flashAttr("putForm", form)
.with(csrf()))	    
// 検証
// ステータスが正常
.andExpect(status().isOk())
// modelの検証
// フォームがエラーとなること
.andExpect(model().hasErrors())
// errorの値
.andExpect(model().attribute("error", "パラメータエラーが発生しました。"))
// putFormにエラーが存在すること
.andExpect(model().attributeExists("putForm"))
// viewの返却
.andExpect(view().name("form"));
}
@Test
public void 権限なし() throws Exception {
// パラメータ設定
PutForm form = new PutForm();
// 実行
mockMvc.perform(post("/diary/update")
.param("update", "update")
.flashAttr("putForm", form)
.with(csrf()))
// 検証
.andExpect(status().is3xxRedirection());
}
}
@Nested
@Sql("/data-unit.sql")
class delete {
@Test
@WithMockUser
public void 削除() throws Exception {
// 実行
mockMvc.perform(post("/diary/delete")
.param("id", "1")
.with(csrf()))	    
// 検証
// リダイレクトすること
.andExpect(redirectedUrl("/diary"));
}
@Test
public void 権限なし() throws Exception {
// 実行
mockMvc.perform(post("/diary/delete")
.param("id", "1")
.with(csrf()))
// 検証
.andExpect(status().is3xxRedirection());
}
}
}

記載できたらJUnitでテストしてみましょう。
検証が全て通ればOKです!

最後に

Contollerのテストコードができました。
これで改修した際にこのテストコードを流して想定通りになるかを確認することで
品質担保の一つになります。

また、テストコードを書いていると、通常のコード内に使用していない変数があったりということがありました。。。気をつけないといけないですね。

次回はJenkinsの設定を実施しようと思います。

参考文献

10.2.4. 単体テストで利用するOSSライブラリの使い方 — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.4.1.RELEASE documentation
Spring Security 18. テスト - リファレンスドキュメント
Spring Security 18. テスト - リファレンスドキュメント18. テスト前へ、
ModelResultMatchers (Spring Framework 6.0.11 API)
declaration: package: org.springframework.test.web.servlet.result, class: ModelResultMatchers
タイトルとURLをコピーしました