6. ユニットテスト

ユニットテストの手順について記述します。 テスト系のリソースは以下のファイル構成になります。

├── main
│      └── resources
│       ├── schema-dev.sql // 開発時使用のDML文。ユニットテストでも使用する。

└── test
    ├── java
    │   ├── com
    │   │   └── example
    │   │       ├── dao
    │   │       │   ├── MstEmployeeDaoTest.java // Daoのテストクラス
    │   │       │   └── MstNewsDaoTest.java
    │   │       ├── dto
    │   │       ├── helper
    │   │       │   └── AuthenticationTestHelper.java // 認証系テスト用のヘルパークラス
    │   │       ├── service
    │   │       │   └── NewsServiceImplTest.java // サービスのテストクラス
    │   │       └── web
    │   │           └── manager
    │   │               ├── NewsManagerListControllerTest.java // コントローラのテストクラス
    │   │               └── NewsManagerRegisterControllerTest.java
    │   └── password
    │       └── PasswordGenerator.java // パスワードをハッシュ化するクラス
    └── resources
        └── com
            └── example
                └── dao
                    ├── data_employee.sql // Daoテストに使用するDDL文
                    └── data_news.sql

6.1. Daoクラスのテスト

Daoテストに使用するDDLを記述します。 実行対象のクラス/メソッドに@SQL アノテーションで指定します。

data_employee.sql

delete from mst_employee;
delete from mst_password;
delete from mst_role;
insert into mst_employee (employee_id, employee_last_name, employee_first_name, role_id) values('01', '管理', '太郎', 'ROLE_ADMIN');
insert into mst_employee (employee_id, employee_last_name, employee_first_name, role_id) values('02', '一般', '二郎', 'ROLE_USER');
insert into mst_employee (employee_id, employee_last_name, employee_first_name, role_id) values('03', '一般', '三郎', 'ROLE_USER');
insert into mst_password (mst_password_id, employee_id, password, generation) values(1, '01', '$2a$10$i7ZAPWh9xNT98pkJ4B6TyuPkPdehn6oZiwOOjm9/GXc3ZNlbUdQLq', '01');
insert into mst_password (mst_password_id, employee_id, password, generation) values(2, '02', '$2a$10$1gJJgBlL75OIjkSgkYPXI.mV7ihEPjxIiCkXKBEc7/r9xUIjZyc9i', '01');
insert into mst_password (mst_password_id, employee_id, password, generation) values(3, '01', '$2a$10$1gJJgBlL75OIjkSgkYPXI.mV7ihEPjxIiCkXKBEc7/r9xUIjZyc9i', '02');
insert into mst_role (role_id, role_name) values('ROLE_ADMIN', '管理者');
insert into mst_role (role_id, role_name) values('ROLE_USER', '一般');
insert into mst_role (role_id, role_name) values('ROLE_ACTUATOR', '運用管理');

Doma2のDAOをテストするサンプルです。 @SQL アノテーションで指定されたSQLを実行してテスト対象のデータを作成し、単体テストを行います。

MstEmployeeDaoTest.java

@RunWith(SpringRunner.class) // テストランナーでSpringRunner.classを指定してSpring Test Contextを使用
@JdbcTest // DB部分のコンポーネントだけロードする ※1
@Import(DomaAutoConfiguration.class) // Doma2のconfigurationをimport
@ComponentScan // テスト対象の実装Beanをscan
@Sql(scripts = "../../../schema-dev.sql") // DMLを実行する。ここではテーブルをDrop&Createしている
@Sql(scripts = "data_employee.sql") // テスト対象のデータをInsertしている
public class MstEmployeeDaoTest {

    @Autowired
    MstEmployeeDao dao;

    @Test
    public void selectAll() {

        List<MstEmployee> mstEmployees = dao.selectAll();
        assertThat(mstEmployees.size(), is(3));
    }

    @Test
    public void selectOne() {

        MstEmployee actual = dao.selectOne("01");
        assertThat(actual.getEmployeeLastName(), is("管理"));
        assertThat(actual.getEmployeeFirstName(), is("太郎"));
        assertThat(actual.getRoleId(), is("ROLE_ADMIN"));

        actual = dao.selectOne("100");
        assertNull(actual);
    }

    @Test
    public void selectUser(){

        UserEntity actual = dao.selectUser("01");
        assertThat(actual.getEmployeeLastName(), is("管理"));
        assertThat(actual.getEmployeeFirstName(), is("太郎"));
        assertThat(actual.getRoleId(), is("ROLE_ADMIN"));
        assertThat(actual.getPassword(), is("$2a$10$1gJJgBlL75OIjkSgkYPXI.mV7ihEPjxIiCkXKBEc7/r9xUIjZyc9i"));

        actual = dao.selectUser("100");
        assertNull(actual);
    }
}

※1 https://docs.spring.io/spring-boot/docs/current/reference/html/test-auto-configuration.html

Doma2自動生成のメソッドですが、insert,update,deleteもテストしておきたいです。

MstNewsDaoTest.java

@RunWith(SpringRunner.class)
@JdbcTest
@Import(DomaAutoConfiguration.class)
@ComponentScan
@Sql(scripts = "../../../schema-dev.sql")
@Sql(scripts = "data_news.sql")
public class MstNewsDaoTest {

    @Autowired
    MstNewsDao dao;

// 中略

    @Test
    public void insert() {
        MstNews news = new MstNews();

        news.setSubject("単体テスト");
        news.setUrl("http://test.url");
        news.setRoleId("ROLE_ADMIN");

        dao.insert(news);

        List<NewsDto> newsDtoList = dao.selectNewsDtoByCond("単体テスト", null, null, getDefaultSelectOptions());

        NewsDto dto = newsDtoList.get(0);
        assertThat(dto.getSubject(), is("単体テスト"));
        assertThat(dto.getRoleId(),is("ROLE_ADMIN"));
        assertThat(dto.getRoleNm(), is("管理者"));
        assertThat(dto.getUrl(), is("http://test.url"));
    }

    @Test
    public  void update() {

        List<MstNews> mstNewsList = dao.selectAll();
        MstNews news = mstNewsList.get(0);

        news.setSubject("更新テスト");
        news.setUrl("http://test.update.url");
        news.setRoleId("ROLE_ADMIN");
        int ver = news.getVersion();

        dao.update(news);

        List<NewsDto> newsDtoList = dao.selectNewsDtoByCond("更新テスト", null, null, getDefaultSelectOptions());

        NewsDto dto = newsDtoList.get(0);
        assertThat(dto.getSubject(), is("更新テスト"));
        assertThat(dto.getRoleId(),is("ROLE_ADMIN"));
        assertThat(dto.getRoleNm(), is("管理者"));
        assertThat(dto.getUrl(), is("http://test.update.url"));
        assertThat(dto.getVersion(), is(ver + 1));
    }

    @Test
    public void delete() {

        List<MstNews> mstNewsList = dao.selectAll();
        int size = mstNewsList.size();
        MstNews mstNews = mstNewsList.get(0);

        dao.delete(mstNews);

        mstNewsList = dao.selectAll();

        assertThat(mstNewsList.size(), is(size - 1));

    }

   private SelectOptions getDefaultSelectOptions() {
        // 最初のページ
        int pageNo = 0;
        // ページあたり件数
        int sizePerPage = 5;
        // offset指定、最大100件、カウントあり
        int offset = pageNo * sizePerPage;
        return SelectOptions.get().offset(offset).limit(100).count();
    }

// 後略

}

6.2. コントローラクラスのテスト

コントローラのテストを実行します。 ここでは、以下をテスト対象とします。

  1. リクエストマッピング
  2. 認証
  3. バリデーションのメッセージ

NewsManagerListControllerTest.java

@RunWith(SpringRunner.class)
@WebMvcTest(value = NewsManagerListController.class) // テスト対象のコントローラを指定
@Import(SecurityConfig.class) // 認証の設定を追加でロード
public class NewsManagerListControllerTest {

    // 擬似的なリクエストをDispatcher Servletにリクエストするモック
    @Autowired
    private MockMvc mvc;

    @MockBean
    NewsService mockService;

    @Before
    public void setup() {

        // セキュリティ設定
        AuthenticationTestHelper.管理者権限の設定();
        // modelAttribute
        Mockito.when(mockService.retrieveRoleIdMap()).thenReturn(new HashMap<String, String>());
    }

    @Test
    public void 重要なお知らせリスト画面_リクエストマッピング() throws Exception {

        this.mvc.perform(
                MockMvcRequestBuilders.get("/manager/news/list")
                        .with(csrf())
        ).andExpect(status().isOk())
        .andExpect(view().name("/manager/news/list/newsList"));
    }

    @Test
    public void 重要なお知らせリスト画面_一般ユーザでエラーになる() throws Exception {

        // セキュリティ設定
        AuthenticationTestHelper.一般権限の設定();
        this.mvc.perform(
                MockMvcRequestBuilders.get("/manager/news/list")
                        .with(csrf())
        ).andExpect(status().is4xxClientError());
    }
}

重要なお知らせリスト画面_リクエストマッピング メソッドでは「/manager/news/list」に対するリクエストに対し、 HTTPステータスの200が返却され、ビュー「/manager/news/list/newsList」が返却されることをテストしています。 Spring Securityが設定されているため、CSRFトークンが設定される必要があるのでwith(csrf()) でCSRFトークンが設定されています。 setup メソッドではAuthenticationTestHelper クラスで認証設定をしています。

AuthenticationTestHelper.java

public class AuthenticationTestHelper {

    public static void 管理者権限の設定() {

        // 認証状態にする
        UserInfo userInfo = new UserInfo();

        userInfo.setId("01");
        userInfo.setEmployeeFirstName("テスト");
        userInfo.setEmployeeLastName("太郎");
        userInfo.setRoleId("ROLE_ADMIN");
        userInfo.setPassword("pwd");

        LoginUserDetails details = new LoginUserDetails(userInfo);

        Authentication authentication =
                new TestingAuthenticationToken(details, null, "ROLE_ADMIN");

        setAuthentication(authentication);

    }

    public static void 一般権限の設定() {

        // 認証状態にする
        UserInfo userInfo = new UserInfo();

        userInfo.setId("01");
        userInfo.setEmployeeFirstName("テスト");
        userInfo.setEmployeeLastName("太郎");
        userInfo.setRoleId("ROLE_USER");
        userInfo.setPassword("pwd");

        LoginUserDetails details = new LoginUserDetails(userInfo);

        Authentication authentication =
                new TestingAuthenticationToken(details, null, "ROLE_USER");

        setAuthentication(authentication);

    }

    private static void setAuthentication(Authentication authentication) {
        SecurityContext securityContext;
        securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(authentication);
        SecurityContextHolder.setContext(securityContext);
    }
}

AuthenticationTestHelperクラスではSecurityContextHolderにSecurityContextを設定し、Spring Securityの認証状態を設定します。

NewsManagerRegisterControllerTest.java

@RunWith(SpringRunner.class)
@WebMvcTest(value = NewsManagerRegisterController.class)
@Import(SecurityConfig.class)
public class NewsManagerRegisterControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    NewsService mockService;

    @Before
    public void setup() {

        AuthenticationTestHelper.管理者権限の設定();
        // modelAttribute
        Mockito.when(mockService.retrieveRoleIdMap()).thenReturn(new HashMap<String, String>());
    }

    @Test
    public void お知らせ登録確認画面_バリデーションチェック() throws Exception {

        MvcResult result = this.mvc.perform(
                MockMvcRequestBuilders.post("/manager/news/register")
                        .param("confirm", "")
                        .param("url", "hoge://a.b")
                        .param("subject", "")
                        .with(csrf())
        )
                .andExpect(status().isOk())
                .andExpect(model().hasErrors())
                .andExpect(view().name("/manager/news/register/newsRegisterInput"))
                .andReturn();

        // エラーメッセージの確認
        String content = result.getResponse().getContentAsString();

        assertThat(content, is(containsString("お知らせURLの形式が正しくありません。")));
        assertThat(content, is(containsString("お知らせ表題が入力されていません。")));
        assertThat(content, is(containsString("権限IDが入力されていません。")));

    }

}

NewsForm.java

/**
 * お知らせ画面フォームクラス.
 */
public class NewsForm {

    /** id */
    private Long id;
    /** ページ */
    private Integer page;
    /** 表題 */
    @NotBlank
    private String subject;
    /** 権限 */
    @NotBlank
    private String roleId;
    /** 権限名 */
    private String roleNm;
    /** URL */
    @NotBlank
    @URL(message = "お知らせURLの形式が正しくありません。")
    private String url;
    /** バージョン */
    private int version;

    // getter.setter
}

ValidationMessages.properties

org.hibernate.validator.constraints.NotBlank.message = {0}が入力されていません。

message.properties

newsForm.subject = お知らせ表題
newsForm.url = お知らせURL
newsForm.roleId = 権限ID

バリデーションチェックの結果、出力されるメッセージをレスポンスの文字列で確認しています。 エラーメッセージはValidationMessages.properties ,messages.properties ,Formクラスが関連して出力されるので、ユニットテストで確認しておいた方が無難です。