3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RESTEasyとJPAとAngularJSを使ってWebアプリを作ってみる5(JPAの単体テスト)

Last updated at Posted at 2015-12-30

RESTEasyとJPAとAngularJSを使ってWebアプリを作ってみる4(JPAでデータ取得)で作成したメソッドを検証するために単体テストを実行します。

データ登録ユーティリティ

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

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();
		}
	}
}

単体テストの基底クラス

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

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
「SUBJECT」シート
SUBJECT.png
「QUESTION」シート
QUESTION.png
「HISTORY」シート
HISTORY.png
「ANSWER」シート
ANSWER.png

UserService の単体テストクラス

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

  • 履歴を持っていないユーザのログイン
  • 直前の履歴では全問正解のユーザのログイン
  • 直前の履歴で誤答のあるユーザのログイン
  • 存在しないアカウントIDのログイン
  • アカウントは存在するがパスワードの異なるログイン
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」も出力対象に含めているので、取得結果が表形式で出力されデバッグ時にはうれしいです。

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アプリを作ってみる6(メール送信)

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?