今回は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());
}
}
}
その他のメソッドも記載します。
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