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メソッドに対するテストコードのみ記載します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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を利用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
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をコピーしました