SpringframeworkのコントローラーをJunitでテスト

#対象読者
・SpringFramework初学者
・Junit初学者
・JunitでSpringframeworkのコントローラーをテストしたい！！！

#プロジェクトver
・Springframework 4.1.6
・Junit 4.1.2
・servlet 3.1.0
・hamcrest 2.2
・mockito 3.3.3

#ファイル構造
```
//メイン層
src.main.java
  |
  +--- jp.co.demo                         
        +--- controller 
        +--- dto
        +--- form
        +--- entity
        +--- mapper
        +--- service
  
//テスト層
src.test.java
  |
  +--- jp.co.demo                         
  |     +--- controller 
  |
src.test.resources
  |
  +--- META-INF
  |           |
  |           +--- sql
  |                  |
  |                  +--- insert_data.sql //テスト用データ作成のSQL
  |
  +--- spring
            |
            +--- mvc-config.xml //Bean定義ファイル

```

#テスト
##SQL実行ファイル
```sql:insert_data.sql

DELETE FROM messages;
DELETE FROM users;

INSERT INTO users(id,account,password,name,branch_id,department_id) VALUES (1,'testuser','$2a$10$i0FlsKe6FiEuViMhclA90uCjCCKeLhtcswz01Rwl9qTIsIY1c.ohO','ALH太郎',1,1);

ALTER TABLE messages MODIFY id int not null; --オートインクリメントリセットするために一時的にオートインクリメントリセット
ALTER TABLE messages AUTO_INCREMENT = 1; --オートインクリメントリセット
ALTER TABLE messages MODIFY id int not null auto_increment; --オートインクリメント付与

INSERT INTO messages(title,text,category,user_id, created_date)VALUES('タイトル','本文','カテゴリ',1,'2020-05-27 01:02:03'); --3
INSERT INTO messages(title,text,category,user_id, created_date)VALUES('タイトル','本文','カテゴリ',1,'2020-05-28 02:03:04'); --4
INSERT INTO messages(title,text,category,user_id, created_date)VALUES('タイトル','本文','カテゴリ',1,'2020-05-23 03:04:05'); --1
INSERT INTO messages(title,text,category,user_id, created_date)VALUES('タイトル','本文','カテゴリ',1,'2020-05-24 04:05:06'); --2
INSERT INTO messages(title,text,category,user_id, created_date)VALUES('タイトル','本文','カテゴリ',1,'2020-05-29 05:06:07'); --5
```


##コントローラーテスト
```java:DemoControllerTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration //コンテキスト使う準備
@ContextConfiguration(locations = "classpath:spring/mvc-config.xml") //どのBean定義ファイルを使うか指定
@Transactional //テスト終了後にロールバックするために定義
public class DemoControllerTest {

	private MockMvc mockMvc;

	@Autowired
	private WebApplicationContext wac; //コンテキストを用意

	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Before
	public void setUp() {
		executeScript("/META-INF/sql/insert_data.sql"); //SQLの実行
		mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
	}

	@Test
	public void test_1ホーム画面表示() throws Exception {
		MvcResult mvcResult = mockMvc.perform(get("/home")) //performでGET通信の振る舞い
				.andDo(print()) //コンソールにプリント
				.andExpect(status().isOk()) //statusコードの確認
				.andExpect(view().name("home")) //Viewが合っているか
				.andExpect(model().attributeExists("allUserMessage")) //対象のモデルが存在するかの確認
				.andExpect(model().attributeExists("userComment")) //対象のモデルが存在するかの確認
				.andExpect(model().attributeExists("deleteCommentForm")) //対象のモデルが存在するかの確認
				.andExpect(model().attributeExists("commentForm")) //対象のモデルが存在するかの確認
				.andExpect(model().attributeExists("userMessageForm")) //対象のモデルが存在するかの確認
				.andReturn(); //結果をmvcResultへ返却

		ModelAndView mav = mvcResult.getModelAndView();
		int[] expectMessageIdList = { 5, 2, 1, 4, 3 }; //昇順確認用

		@SuppressWarnings(value = "unchecked")
		List<UserMessageForm> actualMessageList = ((List<UserMessageForm>) mav.getModel().get("allUserMessage"));

		int expectMessageListSize = 5;
		assertEquals(expectMessageListSize, actualMessageList.size()); //作成したデータと表示されているモデルのListサイズが合っているか

		for (int i = 0; i < 5; i++) {
			assertEquals(expectMessageIdList[i], actualMessageList.get(i).getId()); //メッセージ内容が昇順になっているか？
		}

	}

	@Test
	public void test_2_1ログイン画面表示出来るか() throws Exception {
		mockMvc.perform(get("/login"))
				.andDo(print())
				.andExpect(status().isOk())
				.andExpect(model().attributeExists("loginForm"));
	}

	@Test
	public void test_2_2ログイン出来るか() throws Exception {
		LoginForm loginForm = new LoginForm();
		loginForm.setAccount("testuser");
		loginForm.setPassword("password");

		mockMvc.perform(post("/login").flashAttr("loginForm", loginForm))
				.andDo(print())
				.andExpect(view().name("redirect:/home")) //ログインが成功した時の遷移先
				.andExpect(status().isFound());
	}

	@Test
	public void test_2_3パスワードを間違えた時エラー出力してログイン画面に遷移() throws Exception {
		LoginForm loginForm = new LoginForm();
		loginForm.setAccount("testuser");
		loginForm.setPassword("誤ったパスワード");

		MvcResult mvcResult = mockMvc.perform(post("/login")
				.flashAttr("loginForm", loginForm))
				.andDo(print())
				.andExpect(view().name("login"))
				.andExpect(status().isOk())
				.andExpect(model().attributeHasErrors("loginForm")) //Modelにエラーがあるか
				.andReturn();

		ModelAndView mav = mvcResult.getModelAndView();
		BindingResult result = (BindingResult) mav.getModel()
				.get("org.springframework.validation.BindingResult.loginForm");

		String actualErrorMessage = result.getFieldError("account").getDefaultMessage();
		String expectErrorMessage = "ログインIDまたはパスワードが間違っています";
		assertThat(expectErrorMessage, is(actualErrorMessage)); //エラーメッセージの確認
	}

	@Test
	public void test_2_4存在しないアカウントでログインしようとした時エラーを画面に出力してログイン画面に遷移() throws Exception {
		LoginForm loginForm = new LoginForm();
		loginForm.setAccount("存在しないアカウント");
		loginForm.setPassword("password");

		MvcResult mvcResult = mockMvc.perform(post("/login")
				.flashAttr("loginForm", loginForm))
				.andDo(print())
				.andExpect(status().isOk())
				.andExpect(model().attributeHasErrors("loginForm")) //Modelにエラーがあるか
				.andReturn();

		ModelAndView mav = mvcResult.getModelAndView();
		BindingResult result = (BindingResult) mav.getModel()
				.get("org.springframework.validation.BindingResult.loginForm");

		String actualErrorMessage = result.getFieldError("account").getDefaultMessage();
		String expectErrorMessage = "アカウントが存在しません";
		assertThat(expectErrorMessage, is(actualErrorMessage)); //エラーメッセージの確認
	}

	public void executeScript(String file) {
		Resource resource = new ClassPathResource(file, getClass());

		ResourceDatabasePopulator rdp = new ResourceDatabasePopulator();
		rdp.addScript(resource);
		rdp.setSqlScriptEncoding("UTF-8");
		rdp.setIgnoreFailedDrops(true);
		rdp.setContinueOnError(false);

		Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
		rdp.populate(conn);
	}

}
```

#感想
あくまでもコントローラーの単体テストであり、View側のレンダリング結果を取得してテストする等は考慮していません（またログインフィルター等も掛けていない）。
HtmlUnitを使えば、レンダリング結果を得られるらしいので、別の機会にQiitaに挙げていこうと思います。

#参考文献

[Java + Spring によるテストデータ管理(3)](https://qiita.com/jca02266/items/57008640b1f0c0430453)
[[SpringBoot] Controllerのテストの書き方](https://qiita.com/ryo2132/items/ec10116238e1e1f333a1)
[Springテスト公式ドキュメント](https://spring.pleiades.io/spring-framework/docs/current/spring-framework-reference/testing.html#spring-mvc-test-framework)