Spring Boot2のユニットテストの書き方メモ

## はじめに

spring boot2でユニットテストを書くことになったので、
書き方を整理しようと思います。

Modelにセットした値のテストってどう書くんだっけ？
といったケースで思い出しように使ってもらえたらと思います。

サンプルコードは[GitHub](https://github.com/aoyaman/lean_spring_boot_unit_test)にあげてます。

### レスポンスStatusのテスト

```Java

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

this.mockMvc.perform(get("/")).andDo(print())
        // ステータス「200 OK」を返すかどうか
        .andExpect(status().isOk())
        .andExpect(status().is(200))
```

ドキュメントは[こちら](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/result/StatusResultMatchers.html)のようです。

isOk()でも、is(200)でも、どちらでも同じ結果になります。



### thymeleafのテンプレートファイル

```Java
@Controller
@EnableAutoConfiguration
public class HomeController {
  @GetMapping(path = "/")
  String home(HttpServletRequest request, Model model) {
    // home.htmlを指定する
    return "home";
  }
}
```

といった感じで、thymeleafのテンプレートファイルをreturnしますよね。
これのテストです。

```Java
    // テスト
    this.mockMvc.perform(get("/")).andDo(print())
        // テンプレート「home」を返すかどうか
        .andExpect(view().name("home"))
```

nameメソッドでテストします。
ドキュメントは[こちら](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/result/ViewResultMatchers.html)です。


### Modelのテスト

コントローラーでModelにデータを詰めますよね。

```Java:MyData.java
public class MyData {
  private String strData;
  private int intData;

  public MyData(String strData, int intData) {
    this.strData = strData;
    this.intData = intData;
  }

  public void setStrData(String strData) {
    this.strData = strData;
  }
  public String getStrData() {
    return this.strData;
  }

  public void setIntData(int intData) {
    this.intData = intData;
  }
  public int getIntData() {
    return this.intData;
  }
}
```

```Java:HomeController.java
@Controller
@EnableAutoConfiguration
public class HomeController {
  @GetMapping(path = "/")
  String home(HttpServletRequest request, Model model) {

    // string
    model.addAttribute("test", "this is test");

    // HashMap<String, String>
    HashMap<String, String> map = new HashMap<String, String>();
    map.put("name", "momotaro");
    map.put("age", "23");
    model.addAttribute("map", map);

    // List<String>
    List<String> list = new ArrayList<String>();
    list.add("list1");
    list.add("list2");
    list.add("list3");
    model.addAttribute("list", list);

    // List<MyData>
    List<MyData> list2 = new ArrayList<MyData>();
    list2.add(new MyData("test1", 111));
    list2.add(new MyData("test2", 222));
    model.addAttribute("list2", list2);

    // home.htmlを指定する
    return "home";
  }
}
```

```Java:HomeControllerTest.java
@SpringBootTest(classes = HomeController.class)
@AutoConfigureMockMvc
public class HomeControllerTest {
  @Autowired
  private MockMvc mockMvc;

  @Test
  void home画面() throws Exception {

    HashMap<String, String> map = new HashMap<String, String>();
    map.put("name", "momotaro");
    map.put("age", "23");

    List<String> list = new ArrayList<String>();
    list.add("list1");
    list.add("list2");
    list.add("list3");

    List<MyData> list2 = new ArrayList<MyData>();
    list2.add(new MyData("test1", 111));
    list2.add(new MyData("test2", 222));


    // テスト
    this.mockMvc.perform(get("/")).andDo(print())
        // ステータス「200 OK」を返すかどうか
        .andExpect(status().isOk())
        .andExpect(status().is(200))

        // テンプレート「home」を返すかどうか
        .andExpect(view().name("home"))

        // Modelのテスト。

        // string型
        .andExpect(model().attribute("test", "this is test"))

        // HashMap型
        .andExpect(model().attribute("map", map))

        // List<String>型
        .andExpect(model().attribute("list", hasSize(3)))   // リストのサイズ
        .andExpect(model().attribute("list", hasItem("list1")))   // list1が含まれているか
        .andExpect(model().attribute("list", hasItem("list2")))   // list2が含まれているか
        .andExpect(model().attribute("list", hasItem("list3")))   // list3が含まれているか
        .andExpect(model().attribute("list", contains("list1", "list2", "list3")))  // list1, list2, list3の順で含まれているか
        .andExpect(model().attribute("list", is(list)))  // listと一致しているかどうか

        // List<MyData>型
        .andExpect(model().attribute("list2", hasSize(2)))   // リストのサイズ
        .andExpect(model().attribute("list2",
          hasItem(allOf(hasProperty("strData", is("test1")), hasProperty("intData", is(111))))
        ))   // この組み合わせのデータが含まれているか
        .andExpect(model().attribute("list2",
          hasItem(allOf(hasProperty("strData", is("test2")), hasProperty("intData", is(222))))
        ))   // この組み合わせのデータが含まれているか
        //.andExpect(model().attribute("list2", is(list2)))  // list2と一致しているかどうか -> この書き方は出来ない。エラーになる。

        ;
  }
}
```

このようにattributeメソッドでテストできます。
ドキュメントは[こちら](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/result/ModelResultMatchers.html)です

※List<String>はis()で判定できましたが、List<MyData>はis()で判定できないようです。

### redirectのテスト

POSTの処理でredirectしますよね。

```Java:HomeController.java
  @PostMapping(path = "/testpost")
  public ModelAndView testpost(RedirectAttributes redirectAttributes,  @RequestParam(value = "intvalue", required = false, defaultValue = "0") Integer intvalue) {
    ModelAndView modelAndView = new ModelAndView("redirect:/testget");

    // 入力値のチェック
    if (intvalue <= 0) {
      redirectAttributes.addFlashAttribute("error", "intvalueは0より大きい値でなければいけません");
      return modelAndView;
    }
    // データをセット
    modelAndView.addObject("value", intvalue);
    return modelAndView;
  }
```

```Java:HomeControllerTest.java
  @Test
  void testpost() throws Exception {
    // パラメータを渡さなかったらチェックに引っかかる
    this.mockMvc.perform(post("/testpost")).andExpect(redirectedUrl("/testget"))
        .andExpect(flash().attribute("error", "intvalueは0より大きい値でなければいけません"));

    // パラメータを渡したらチェックを通る
    this.mockMvc.perform(post("/testpost").param("intvalue", "5"))
        .andExpect(redirectedUrl("/testget?value=5"));
  }
```

redirectedUrl()を使ってリダイレクト先のURLをテストします。

flash()を使ってredirectAttributes.addFlashAttribute()のテストをします。

POSTで受け取るパラメータはparam()で指定できます。


### 特定のメソッドが特定の引数でコールされたかどうかをテスト

ControllerでPOSTを受け取ったときに
何かしらの処理を行うと思います。そのテストです。

ここでは、特定のServiceをControllerにDIしておいて、
そのServcieのメソッドをPost処理の中でコールすることのテストとします。


```Java:MyService.java
@Service
public class MyService {
  public void test(int value) {
    System.out.println("MyService.test()..." + value);
  }
}
```

```Java:HomeController.java

@Controller
@EnableAutoConfiguration
public class HomeController {

  @Autowired
  MyService myService;

  @PostMapping(path = "/testpost2")
  public String testpost2(@RequestParam(value = "intvalue", required = false, defaultValue = "0") Integer intvalue) {

    // MyServiceのtest()をコール
    myService.test(intvalue);

    return "redirect:/";
  }
}
```

```Java:HomeControllerTest.java
@SpringBootTest(classes = HomeController.class)
@AutoConfigureMockMvc
public class HomeControllerTest {
  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private MyService myService;

  @Test
  void testpos2() throws Exception {
    // redirectUrlチェック
    this.mockMvc.perform(post("/testpost2").param("intvalue", "5")).andExpect(redirectedUrl("/"));

    // MyServiceのtest()メソッドが引数の値5でコールされたかどうかをテスト
    verify(this.myService, times(1)).test(5);
  }
}

```



```Java
verify(this.myService, times(1)).test(5);
```
と書くと、
「MyServiceクラスのtestメソッドを引数の値5で１回コールされた」
というテストになります。

２回コールされたかどうかのテストは

```Java
verify(this.myService, times(2)).test(5);
```

となりますし、

引数の値が10であって欲しい時は

```Java
verify(this.myService, times(1)).test(10);
```

と書きます。

これによって、
コントローラーのテストは
パラメータの値をチェックして処理を切り分けることと、
他サービスの関数にパラメータの値を渡すこと
の２種類に絞られるかと思います。


### Getのパラメータのテスト

Postのパラメータはparams()で指定しましたが、
GetのパラメータはURLで指定します。

```Java:HomeController.java
  @GetMapping(path = "/testget")
  String testget(@RequestParam(value = "value", required = false, defaultValue = "0") Integer value, @ModelAttribute("error") String error, Model model) {

    model.addAttribute("strError", error);
    model.addAttribute("intValue", value);

    // testget.htmlを指定する
    return "testget";
  }
```

```Java:HomeControllerTest.java
  @Test
  void testget() throws Exception {
    this.mockMvc.perform(get("/testget?value=5&error=SuperError")).andDo(print())
        // ステータス「200 OK」を返すかどうか
        .andExpect(status().isOk())

        // テンプレート「testget」を返すかどうか
        .andExpect(view().name("testget"))

        // Modelのテスト。
        .andExpect(model().attribute("intValue", 5))
        .andExpect(model().attribute("strError", "SuperError"))
        ;
  }
```



## おわりに

まだまだ、spring bootでのテストを書き始めた時点でのメモですので、
今後も加筆していきたいと思います。

サンプルコードは[GitHub](https://github.com/aoyaman/lean_spring_boot_unit_test)にあげてます。

おわり。