RESTEasyとJPAとAngularJSを使ってWebアプリを作ってみる５（JPAの単体テスト）

[RESTEasyとJPAとAngularJSを使ってWebアプリを作ってみる４（JPAでデータ取得）](http://qiita.com/1roh/items/0c71441371f3ba76b3c9)で作成したメソッドを検証するために単体テストを実行します。

# データ登録ユーティリティ
単体テストをするにはデータがないと話にならないので、エクセルデータを登録するユーティリティクラスを作成します。
シート名にテーブル名、各シートの1行目にカラム名、2行目以降は登録データを入力し、一番右のシートからデータを削除（外部キーを指定しているため）した後に、左のシートから順に登録していく仕様にします。
なお、エンティティマネジャーは EntityManager#createNativeQuery(String) で生のSQLを実行できるみたいです。
src/test/java - homework.tools 下に ExcelDataImporter を作ります。

```java:ExcelDataImporter.java
package homework.tools;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;

import homework.utils.Configure;
import homework.utils.Crypter;
import homework.utils.SystemException;

/**
 * エクセルに記載したデータをテーブルに登録する。
 * @author satake
 */
public class ExcelDataImporter {

	/**
	 * エクセルを読み込みデータをテーブルに登録する。
	 * @param manager	エンティティマネジャー
	 * @param xlsFile	エクセルファイル名
	 */
	public void importRecords(EntityManager manager, String xlsFile) {
		try {
			// クラスパスからファイル名を探してワークブックを取得します
			Workbook book = WorkbookFactory.create(this.getClass().getClassLoader().getResourceAsStream(xlsFile));
			// シート名（テーブル名）の一覧を取得します
			List<Sheet> sheets = new ArrayList<>();
			for (int sheetNo = 0; sheetNo < book.getNumberOfSheets(); ++sheetNo) {
				sheets.add(book.getSheetAt(sheetNo));
			}
			// データを削除します
			for (int i = sheets.size() - 1; i >= 0; i--) {
				deleteTable(manager, sheets.get(i).getSheetName());
			}
			// データを登録します
			for (Sheet sheet : sheets) {
				List<String> columns = getColumns(sheet);
				insertRecords(manager, sheet, columns);
			}
		}
		catch (Exception e) {
			logger.error(e);
			throw new SystemException(e);
		}
	}

	/**
	 * テーブルデータを削除する。
	 * @param manager	エンティティマネジャー
	 * @param tableName
	 */
	private void deleteTable(EntityManager manager, String tableName) {
		String sql = "DELETE FROM " + tableName;
		manager.createNativeQuery(sql).executeUpdate();
	}

	/**
	 * カラム名を取得する。
	 * @param sheet		エクセルシート
	 * @return	カラム名のリスト
	 */
	private List<String> getColumns(Sheet sheet) {
		List<String> columns = new ArrayList<>();
		// 1行目で入力がなくなるまでをカラム名として一覧に保持します
		Row row = sheet.getRow(0);
		for (int colNo = 0; colNo < Short.MAX_VALUE; colNo++) {
			Cell cell = row.getCell(colNo);
			if (cell == null) {
				break;
			}
			columns.add(cell.getStringCellValue());
		}
		return columns;
	}

	/**
	 * シートのレコードデータを登録する。
	 * @param manager	エンティティマネジャー
	 * @param sheet		エクセルシート
	 * @param columns	カラム名のリスト
	 */
	private void insertRecords(EntityManager manager, Sheet sheet, List<String> columns) {
		// ユーザ情報はパスワードを暗号化するので区別します
		boolean isUserTable = StringUtils.equals(sheet.getSheetName().toUpperCase(), "USERS");
		// 未入力の行になるまで処理を繰り返します
		for (int rowNo = 1; rowNo < Short.MAX_VALUE; rowNo++) {
			Row row = sheet.getRow(rowNo);
			if (row == null) {
				break;
			}
			StringBuilder sql = new StringBuilder();
			sql.append("INSERT INTO ").append(sheet.getSheetName()).append(" (");
			for (String column : columns) {
				sql.append(column).append(",");
			}
			sql.delete(sql.length() - 1, sql.length());
			sql.append(") VALUES (");
			for (int colNo = 0; colNo < columns.size(); colNo++) {
				Cell cell = row.getCell(colNo);
				if (cell != null) {
					if (cell.getCellType() == Cell.CELL_TYPE_NUMERIC) {
						sql.append(cell.getNumericCellValue()).append(",");
					}
					else {
						sql.append("'");
						// ユーザ情報テーブルでパスワードカラムの場合は暗号化します
						if (isUserTable && StringUtils.equals(columns.get(colNo).toUpperCase(), "PASSWORD")) {
							sql.append(Crypter.encrypt(cell.getStringCellValue()));
						}
						else {
							sql.append(cell.getStringCellValue());
						}
						sql.append("',");
					}
				}
				else {
					sql.append("null,");
				}
			}
			sql.delete(sql.length() - 1, sql.length());
			sql.append(")");
			manager.createNativeQuery(sql.toString()).executeUpdate();
		}
	}
}
```

# 単体テストの基底クラス
単体テスト時の環境変数の設定とかあるので、単体テストの基底クラスを作成します。
単体テストのデータは毎回きれいに使用したいので、テスト終了時にロールバックさせます。

```java:BaseTest.java
package homework.service;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import javax.persistence.EntityManager;

import org.junit.After;
import org.junit.Before;

import homework.tools.ExcelDataImporter;
import homework.utils.EMProducer;
import homework.utils.SystemException;

public class BaseTest {

	static {
		// テスト用データベースを指定
		System.setProperty("jpaUnitName", "homework_ut");
		// log4jdbcのログデリゲーターの設定
		System.setProperty("log4jdbc.spylogdelegator.name", "net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator");
		// log4jdbcのログはINFOレベルでいろいろ出力していて情報が多いので、
		// いったん全体を「ERROR」のみにして、実行SQLとダンプを出力するようにロガーを設定
		System.setProperty("org.slf4j.simpleLogger.log.jdbc", "ERROR");
		System.setProperty("org.slf4j.simpleLogger.log.jdbc.sqltiming", "INFO");
		System.setProperty("org.slf4j.simpleLogger.log.jdbc.resultsettable", "INFO");
	}

	/** エンティティマネジャー */
	protected EntityManager manager = EMProducer.createManager();
	/** エクセルデータ登録ユーティリティ */
	private ExcelDataImporter importer = new ExcelDataImporter();

	@Before
	public void before() {
		// トランザクションを開始する
		manager.getTransaction().begin();
	}

	@After
	public void after() {
		// トランザクションをロールバックする
		manager.getTransaction().rollback();
	}

	/**
	 * サービスクラスのインスタンスを生成する。
	 * @param clazz	対象クラス
	 * @return		対象クラスのインスタンス
	 */
	protected <T> T createService(Class<T> clazz) {
		try {
			Constructor<T> constructor = clazz.getConstructor(EntityManager.class);
			T instance = constructor.newInstance(manager);
			return instance;
		}
		catch (InvocationTargetException e) {
			throw new SystemException(e);
		}
		catch (Exception e) {
			throw new SystemException(e);
		}
	}

	/**
	 * テストデータを登録する。
	 * @param xlsFile	テストデータが記載されたエクセルファイル
	 */
	protected void loadTestData(String xlsFile) {
		importer.importRecords(manager, xlsFile);
	}
}
```

createService はテストクラスで生成したエンティティマネジャーをサービスクラスのエンティティマネジャーにするように、サービスクラスにコンストラクタを追加しておいた。


# テストデータ
src/test/resources に TestUserServiceData.xlsx を作成します。
「USERS」シート
![USERS.png](https://qiita-image-store.s3.amazonaws.com/0/70918/3345926c-916b-a179-5739-072f6fe6b3c1.png)
「SUBJECT」シート
![SUBJECT.png](https://qiita-image-store.s3.amazonaws.com/0/70918/4f5d5c23-f8e4-634a-eb27-951a24d8868e.png)
「QUESTION」シート
![QUESTION.png](https://qiita-image-store.s3.amazonaws.com/0/70918/9178719f-f107-9ff2-7d91-445289ba8dd2.png)
「HISTORY」シート
![HISTORY.png](https://qiita-image-store.s3.amazonaws.com/0/70918/bde46d82-cd4a-cb5d-9b3b-98c5c45e63db.png)
「ANSWER」シート
![ANSWER.png](https://qiita-image-store.s3.amazonaws.com/0/70918/1b476ddc-79e5-a2a6-75f1-96dc981f1607.png)

# UserService の単体テストクラス
上で作成した BaseTest を継承して作成します。
テストメソッド内でテスト対象を実行する前に BaseTest#loadTestData(String) を呼び出すことを除けば、特に変哲のないテストコードになります。
ざっくりと以下のケースで確認します。

- 履歴を持っていないユーザのログイン
- 直前の履歴では全問正解のユーザのログイン
- 直前の履歴で誤答のあるユーザのログイン
- 存在しないアカウントIDのログイン
- アカウントは存在するがパスワードの異なるログイン

```java:UserServiceTest.java
package homework.service;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.util.ArrayList;

import org.junit.Test;

import homework.dto.Request;
import homework.dto.Responce;
import homework.entity.Answer;
import homework.entity.Question;
import homework.entity.User;
import homework.utils.Crypter;
import homework.utils.DateUtil;

/**
 * {@link UserService} の単体テストクラス。
 * @author satake
 */
public class UserServiceTest extends BaseTest {

	/**
	 * 履歴を持っていないユーザのログイン。
	 * <p>
	 * ユーザ情報と空の回答一覧が返却される。
	 * </p>
	 */
	@Test
	public void testLogin1() {
		loadTestData("TestUserServiceData.xlsx");
		User req = new User();
		req.setAccountId("account1");
		req.setPassword("passwd1");
		UserService service = createService(UserService.class);
		Responce res = service.login(req);
		assertThat("account1", is(res.getUser().getAccountId()));
		assertThat(Crypter.encrypt("passwd1"), is(res.getUser().getPassword()));
		assertThat("試験", is(res.getUser().getLastName()));
		assertThat("一郎", is(res.getUser().getFirstName()));
		assertThat("テスト", is(res.getUser().getLastNameKana()));
		assertThat("イチロウ", is(res.getUser().getFirstNameKana()));
		assertThat("test1@test.com|@|test2@test.com", is(res.getUser().getNoticeMail()));
		assertThat(0, is(res.getAnswers().size()));
	}

	/**
	 * 直前の履歴では全問正解のユーザのログイン。
	 * <p>
	 * ユーザ情報と空の回答一覧が返却される。
	 * </p>
	 */
	@Test
	public void testLogin2() {
		loadTestData("TestUserServiceData.xlsx");
		User req = new User();
		req.setAccountId("account2");
		req.setPassword("passwd2");
		UserService service = createService(UserService.class);
		Responce res = service.login(req);
		assertThat(100002, is(res.getUser().getUserId()));
		assertThat(0, is(res.getAnswers().size()));
	}

	/**
	 * 直前の履歴で誤答のあるユーザのログイン。
	 * <p>
	 * ユーザ情報と誤答した回答一覧が返却される。
	 * </p>
	 */
	@Test
	public void testLogin3() {
		loadTestData("TestUserServiceData.xlsx");
		User req = new User();
		req.setAccountId("account3");
		req.setPassword("passwd3");
		UserService service = createService(UserService.class);
		Responce res = service.login(req);
		assertThat(100003, is(res.getUser().getUserId()));
		assertThat(3, is(res.getAnswers().size()));
		assertThat("質問14", is(res.getAnswers().get(0).getQuestion().getQuestion()));
		assertThat("解答14", is(res.getAnswers().get(0).getQuestion().getCorrect()));
		assertThat("誤答14", is(res.getAnswers().get(0).getAnswer()));
		assertThat("質問15", is(res.getAnswers().get(1).getQuestion().getQuestion()));
		assertThat("解答15", is(res.getAnswers().get(1).getQuestion().getCorrect()));
		assertThat("誤答15", is(res.getAnswers().get(1).getAnswer()));
		assertThat("質問16", is(res.getAnswers().get(2).getQuestion().getQuestion()));
		assertThat("解答16", is(res.getAnswers().get(2).getQuestion().getCorrect()));
		assertThat("誤答16", is(res.getAnswers().get(2).getAnswer()));
	}

	/**
	 * 存在しないアカウントIDのログイン。
	 * <p>
	 * ユーザ情報がnullで返却される。
	 * </p>
	 */
	@Test
	public void testLogin4() {
		loadTestData("TestUserServiceData.xlsx");
		User req = new User();
		req.setAccountId("account_not_exist");
		req.setPassword("passwd");
		UserService service = createService(UserService.class);
		Responce res = service.login(req);
		assertNull(res.getUser());
	}

	/**
	 * アカウントは存在するがパスワードの異なるログイン。
	 * <p>
	 * ユーザ情報がnullで返却される。
	 * </p>
	 */
	@Test
	public void testLogin5() {
		loadTestData("TestUserServiceData.xlsx");
		User req = new User();
		req.setAccountId("account1");
		req.setPassword("wrong_passwd");
		UserService service = createService(UserService.class);
		Responce res = service.login(req);
		assertNull(res.getUser());
	}
}
```

なお、JPAのログとlog4jdbcの違いは以下のようになります。
Hibernate: となっているものが JPA が出力した標準出力への内容
[main] となっているものがlog4jdbcが出力した内容です。単体テスト基底クラスで「org.slf4j.simpleLogger.log.jdbc.resultsettable」も出力対象に含めているので、取得結果が表形式で出力されデバッグ時にはうれしいです。

```text
Hibernate: 
    select
        user0_.USER_ID as USER_ID1_4_,
        user0_.ACCOUNT_ID as ACCOUNT_2_4_,
        user0_.BIRTH_DATE as BIRTH_DA3_4_,
        user0_.FIRST_NAME as FIRST_NA4_4_,
        user0_.FIRST_NAME_KANA as FIRST_NA5_4_,
        user0_.LAST_NAME as LAST_NAM6_4_,
        user0_.LAST_NAME_KANA as LAST_NAM7_4_,
        user0_.NOTICE_MAIL as NOTICE_M8_4_,
        user0_.password as password9_4_ 
    from
        users user0_ 
    where
        user0_.ACCOUNT_ID=? 
        and user0_.password=?
[main] INFO jdbc.sqltiming - select user0_.USER_ID as USER_ID1_4_, user0_.ACCOUNT_ID as ACCOUNT_2_4_, user0_.BIRTH_DATE 
as BIRTH_DA3_4_, user0_.FIRST_NAME as FIRST_NA4_4_, user0_.FIRST_NAME_KANA as FIRST_NA5_4_, 
user0_.LAST_NAME as LAST_NAM6_4_, user0_.LAST_NAME_KANA as LAST_NAM7_4_, user0_.NOTICE_MAIL 
as NOTICE_M8_4_, user0_.password as password9_4_ from users user0_ where user0_.ACCOUNT_ID='account3' 
and user0_.password='mxVGO3kSXxU=' 
 {executed in 0 msec}
[main] INFO jdbc.resultsettable - 
|--------|-----------|-----------|-----------|----------------|----------|---------------|---------------|-------------|
|user_id |account_id |birth_date |first_name |first_name_kana |last_name |last_name_kana |notice_mail    |password     |
|--------|-----------|-----------|-----------|----------------|----------|---------------|---------------|-------------|
|100003  |account3   |2004-01-03 |三郎         |サブロウ            |試験        |テスト            |test4@test.com |mxVGO3kSXxU= |
|--------|-----------|-----------|-----------|----------------|----------|---------------|---------------|-------------|

Hibernate: 
    select
        answer0_.ANSWER_ID as ANSWER_I1_0_,
        answer0_.answer as answer2_0_,
        answer0_.CORRECT_WRONG as CORRECT_3_0_,
        answer0_.HISTORY_ID as HISTORY_4_0_,
        answer0_.QUESTION_ID as QUESTION5_0_ 
    from
        Answer answer0_ 
    where
        answer0_.HISTORY_ID=(
            select
                max(history1_.HISTORY_ID) 
            from
                History history1_ 
            where
                history1_.USER_ID=?
        ) 
    order by
        answer0_.ANSWER_ID
[main] INFO jdbc.sqltiming - select answer0_.ANSWER_ID as ANSWER_I1_0_, answer0_.answer as answer2_0_, answer0_.CORRECT_WRONG 
as CORRECT_3_0_, answer0_.HISTORY_ID as HISTORY_4_0_, answer0_.QUESTION_ID as QUESTION5_0_ 
from Answer answer0_ where answer0_.HISTORY_ID=(select max(history1_.HISTORY_ID) from History 
history1_ where history1_.USER_ID=100003) order by answer0_.ANSWER_ID 
 {executed in 0 msec}
[main] INFO jdbc.resultsettable - 
|----------|-------|--------------|-----------|------------|
|answer_id |answer |correct_wrong |history_id |question_id |
|----------|-------|--------------|-----------|------------|
|100005    |解答11   |1             |100002     |100011      |
|100006    |解答12   |1             |100002     |100012      |
|100007    |解答13   |1             |100002     |100013      |
|100008    |誤答14   |0             |100002     |100014      |
|100009    |誤答15   |0             |100002     |100015      |
|100010    |誤答16   |0             |100002     |100016      |
|----------|-------|--------------|-----------|------------|

このあとAnswerが保持しているQuestionの情報を取得するために、SQLがレコード数分実行されていました。
```

オールグリーンになったら、他に必要なサービスメソッドとその単体テストを作成していきます。

[RESTEasyとJPAとAngularJSを使ってWebアプリを作ってみる６（メール送信）](http://qiita.com/1roh/items/d3a36547b31abfc0ed26)へ