RESTEasyとJPAとAngularJSを使ってWebアプリを作ってみる4(JPAでデータ取得)で作成したメソッドを検証するために単体テストを実行します。
データ登録ユーティリティ
単体テストをするにはデータがないと話にならないので、エクセルデータを登録するユーティリティクラスを作成します。
シート名にテーブル名、各シートの1行目にカラム名、2行目以降は登録データを入力し、一番右のシートからデータを削除(外部キーを指定しているため)した後に、左のシートから順に登録していく仕様にします。
なお、エンティティマネジャーは EntityManager#createNativeQuery(String) で生のSQLを実行できるみたいです。
src/test/java - homework.tools 下に ExcelDataImporter を作ります。
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();
}
}
}
単体テストの基底クラス
単体テスト時の環境変数の設定とかあるので、単体テストの基底クラスを作成します。
単体テストのデータは毎回きれいに使用したいので、テスト終了時にロールバックさせます。
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」シート
「SUBJECT」シート
「QUESTION」シート
「HISTORY」シート
「ANSWER」シート
UserService の単体テストクラス
上で作成した BaseTest を継承して作成します。
テストメソッド内でテスト対象を実行する前に BaseTest#loadTestData(String) を呼び出すことを除けば、特に変哲のないテストコードになります。
ざっくりと以下のケースで確認します。
- 履歴を持っていないユーザのログイン
- 直前の履歴では全問正解のユーザのログイン
- 直前の履歴で誤答のあるユーザのログイン
- 存在しないアカウントIDのログイン
- アカウントは存在するがパスワードの異なるログイン
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」も出力対象に含めているので、取得結果が表形式で出力されデバッグ時にはうれしいです。
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がレコード数分実行されていました。
オールグリーンになったら、他に必要なサービスメソッドとその単体テストを作成していきます。