SAStrutsでリダイレクトした後に一度だけメッセージを表示する

知識不足からちょっとハマったのでメモ。

SAStrutsでリダイレクトした画面で一度だけメッセージを表示したい場合の処理コード。用途としては、処理完了後の完了メッセージを表示したいというケースで使える。

Actionクラス
public HttpSession session;

@Execute
public String finish() {
    ...
    ActionMessages messages = new ActionMessages();
    messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("xxx"));
    ActionMessagesUtil.saveMessages(session, messages);

    return "xxx.jsp";
}
JSP
<html:messages id="message" message="true">
    <p>メッセージ:${message}</p>
</html:messages>

今回、のmessage="true"を入れていなかったため、メッセージが表示されないという事態に陥ってしまった。この属性は、通常のメッセージの場合(saveMessages()を呼んだ場合)はtrue、エラーメッセージ(saveErrors()を呼んだ場合)はfalseに設定するらしい。

”普通のメッセージですか?”という属性なのね。個人的にはerror="true"とか指定できたほうが分かりやすい気がする。

「メール見出しジェネレータ」を再び公開

以前作った「メール見出しジェネレータ」というサービスを自宅サーバで公開。まぁ、HTML+JavaScriptだけで動くんで、サービスというよりツールですね。興味が出たら使ってみてください。

トビの開発室(トップ)
メール見出しジェネレータ

ちなみに、自宅サーバはCentOS5(Linux)で構築。LinuxWindowsに比べて敷居が高く大変だと思っていたけどそうでもなかった。しかし、CUIだとコードを書くのはちょっとしんどいね。複数ファイルの編集とか大変。

S2UnitでDB削除のテスト

S2UnitによるDBのテストの最後は、削除(DELETE)。今回対象となるのは、論理削除ではなく物理削除。論理削除は、更新系の処理になるのでS2UnitでDB更新のテストを参照のこと。

テストの考え方

  1. テスト用のデータを定義したExcelを用意
  2. ExcelからDBにテスト用データをセットアップ
  3. 削除処理を実行
  4. 削除されたはずのレコードを主キーで取得し、取得できないことを確認

今回作成したソース

EmployeeService.java
public void deleteByEmployeeId(Integer employeeId) {

    Employee entity = jdbcManager.from(Employee.class).where("employeeId = ?", employeeId).getSingleResult();

    jdbcManager.delete(entity).execute();
}

テスト対象のメソッド。引数で主キーを受け取り、それをキーにレコードを削除している。

EmployeeServiceTest.java
public void test_deleteByEmployeeIdTx() {

    readXlsWriteDb("EmployeeServiceTestInit.xls");

    employeeService.deleteByEmployeeId(1);

    Employee entity = jdbcManager.from(Employee.class).where("employeeId = ?", 1).getSingleResult();

    assertNull(entity);    // レコードは削除されたはずなので、nullが取得されること
}

テストメソッド。まず、readXlsWriteDb()でDBにテストデータをセットアップした後、テスト対象のメソッドを呼び出している。その後、S2JDBC(jdbcManager)を使用して、削除されたはずのレコードを主キーを元に取得している。そして、その結果がnullであることを確認してテスト終了。

補足

今回のケースでは1件削除するだけであったが、複数のテーブルのレコードを一気に削除するケースも多いと思う(ユーザが退会して〜とか)。そうした場合は、テストコードが冗長になるので、privateなショートカットメソッドを用意して、読みやすくしておくのも手かもしれない。私はテストコードもリファクタリングすべきだと思っている。

assertNull(getEmployeeById(1));
assertNull(getEmployeeById(2));
assertNull(getEmployeeById(3));

assertNull(getDepartmentById(1));
assertNull(getDepartmentById(2));

あるいは、もっと進めて

String[] assertDelete = { "Employee:1,2,3", "Department:1,2" };
assertDeleteRecord(assertDelete);

こんな風に書けるようにするのも悪くないかもしれない。

S2UnitでのDBテストを終えて

今回で、S2Unitを使用したDBのテストは終わりです。Excelからテスト用データをDBにセットアップしたり、自動ロールバック機能があったりと、S2UnitはDBの単体テストをかなり楽にしてくれると思いました。

DBもリファクタリングが行われるこのご時世。DBの単体テスト自動化も「当たり前」になる時代は既に訪れているのかもしれません。

懸案になっていた問題について

そういえばすっかり忘れていたけど、以前「SAStrtusで初回リクエスト時にクラスからアノテーション情報が取得できない?」という問題を取り上げた。

とりあえず、この問題はCoolDeployの設定だと発生しないことが分かった。うーん、S2ContainerのHotDeploy機能のバグなのかなぁ。

CoolDeployなら問題ないことが分かったので、調査は一旦打ち切ろうと思う。

S2UnitでDB更新のテスト

昨日のエントリーに続き、S2Unitを使ったDBのテスト方法を見ていきたいと思います。今回は、更新(UPDATE)のテストです。

テストの考え方

  1. テスト用のデータを定義したExcel①を用意
  2. テスト結果期待値のデータを定義したExcel②を用意
  3. Excel①でDBにテスト用データをセットアップ
  4. 更新処理を実行
  5. DBの内容がExcel②と一致していることを確認

今回作成したリソース

EmployeeService.java
public void changeDepartmentId(Integer srcDepartmentId, Integer dstDepartmentId) {

    List<Employee> employeeList = jdbcManager.from(Employee.class)
                                    .where("departmentId = ?", srcDepartmentId)
                                    .getResultList();

    for (Employee employee : employeeList) {
        employee.departmentId = dstDepartmentId;
    }

    jdbcManager.updateBatch(employeeList).execute();
}

今回のテスト対象メソッド。特定の部署の社員を一気に別の部署に移すというレアなケースでの利用を想定したメソッド。

EmployeeServiceTest.java
public void test_changeDepartmentIdTx() {

    readXlsWriteDb("EmployeeServiceTestInit.xls");

    DataSet exp = readXls("EmployeeServiceTest_changeDepartmentId_Assert.xls");

    employeeService.changeDepartmentId(1, 2);

    assertEquals(exp, reload(exp));    // 期待値Excelと実際のDBの内容が一致すること
}

テストメソッド。まず検索のテストと同様、readXlsWriteDb()でテスト用データをDBにセットアップしている。その後、readXls()でテストの期待値が定義されたExcelからDataSetを取得している。そして、更新処理を実行。最後に、期待値とDBの内容が一致しているか確認している。

reload()は、主キーを元にして最新のレコードをDBから取得するメソッド。SqlReaderで同様のことができると思ったんだけど、なぜかassertEquals()で失敗してしまった。まぁ、reload()を使ったほうが簡単だからこの方法でいいのだけれど、なぜ失敗するのか気にはなる。

EmployeeServiceTest_changeDepartmentId_Assert.xls


テストの期待値を定義したExcel。変更箇所は分かりやすいように背景を黄色に。

あんまり関係ない余談

社員が特定の部署に所属するという情報を管理する方法として、社員テーブルに部署IDを保持するのはあまり良くないと言われている。所属は関係を示す1つの情報なので、所属テーブルとして抜き出したほうが良い。そうしておけば、所属の履歴を管理したいといった際に、所属テーブルに所属開始日と所属終了日のカラムを増やすだけで対応できる。

S2UnitでDB検索のテスト

CRUD(Create Reference Update Delete)ってなんて読み方が一般的なんだろう。私は”クラッド”なのだけれど”クルド”って読む人もいるらしい。これは、情報は”生成”され、”参照”され、”更新”され、”削除”される、って意味の言葉だけど、この中で何が一番多いかと言えば、言わずもがな”参照”。ということで、今回はS2Unitを用いてDB検索のテストをしたいと思います。

テストの考え方

  1. テスト用の空のDBを用意する。
  2. テストの為に必要なデータをExcelから読み取ってテーブルに反映。
  3. 検索処理を実行(←これが正しいかテスト)
  4. 検索結果で取得されたエンティティの主キーが意図したものかを確認する。

今回作成したリソース

EmployeeService.java
public class EmployeeService {

    public JdbcManager jdbcManager;

    public List<Employee> findByDepartmentId(Integer departmentId) {

        List<Employee> result = jdbcManager.from(Employee.class)
                                    .where("departmentId = ?", departmentId)
                                    .getResultList();
        return result;
    }
}

部署IDを元に社員レコードを取得しようというこのメソッドが今回のテスト対象。しかし、S2JDBCって知らない人にも、どういう処理をやってるのかが理解しやすいよね。

EmployeeServiceTest.java
public class EmployeeServiceTest extends S2TestCase {

    private EmployeeService employeeService;

    public void setUp() {
        include("test.dicon");
    }

    public void test_findByDepartmentIdTx() {

        readXlsWriteDb("EmployeeServiceTestInit.xls");

        List<Employee> employeeList = employeeService.findByDepartmentId(1);

        // EmployeeId=1,2のレコードが取得されること
        assertEquals(2, employeeList.size());
        assertEquals(1, employeeList.get(0).employeeId.intValue());
        assertEquals(2, employeeList.get(1).employeeId.intValue());
    }
}

EmployeeServiceのテストクラス。まず、readXlsWriteDb()で、引数に指定されたパスのExcelファイルを読み取ってテーブルに書き込んでいる。その後、テスト対象のメソッドを呼び出し、結果エンティティ(レコード)が正しいことを確認している。カラム全てを確認するのは効率が悪いので、主キーのみ確認している。

そういえば昨日のエントリーでは書き忘れたけど、S2TestCaseを継承したテストクラスでは、メソッドの最後に"Tx"を付けておくとメソッド終了時に自動的にDBをロールバックしてくれる。こうすれば、テーブルを汚さないでテストが行える。”立つ鳥跡を濁さず”ってやつだね。

EmployeeServiceTestInit.xls



シートは、社員テーブル用と部署テーブル用の2枚で構成。今回のテストでは、社員テーブルしか使わないけど、社員テーブルでは部署テーブルを参照しているので、外部キーで参照している部署レコードが存在しないと挿入できない。readXlsWriteDb()ではシートの定義順に挿入処理が行われるそうなので、今回は部署テーブル用のシートを先にしている。

テスト結果

スクリーンショットは載せないけどグリーンです。ところで、今回は一発でグリーンになったのだけれど、やっぱり最初からグリーンだと逆に不安になるね。テスト駆動で、レッド→グリーン→リファクタリング、という考え方は正しいのかも。

補足

readXlsWriteDb()は、テストクラスと同じパッケージに置いておけば、今回のようにファイル名だけでOK。テストリソースはテストクラスに近い位置にあったほうがいいと思うので、このやり方を推奨。

あと、Eclipseを使っている場合の注意として、パッケージエクスプローラの表示を更新しないと、Excelファイルを更新しても正しく認識されないことがある(たぶんEclipseのキャッシュ機能のせい)。なので、テスト前にパッケージエクスプローラでF5を押すなどして更新するか、ワークスペースを常に最新に更新する設定にしておくと良い。

S2UnitでDB挿入のテスト - DBの内容をExcelに書き出す

Seasar2では、SqlReaderとXlsWriterというクラスが用意されていて、DBのテストが簡単に行えるようになっている。

使い方

重要な部分だけみるとこれだけ。

XlsWriter writer = new XlsWriter("FILE_PATH");
sqlReader.addTable("EMPLOYEE");
sqlReader.addTable("DEPARTMENT");
writer.write(sqlReader.read());
  • 出力するExcelファイルのパスを指定してXlsWriterを生成する。
  • Excelに出力したいテーブルの名前を、SqlReader.addTable("tableName")で追加。
  • SqlReader.read()でDataSetを取得し、それをXlsWriter.write()で書込み。

これはExcelに出力して確認する方法になるので、自動化ではなく目視による確認ということになる。まだ試していないが、SqlReader.read()で取得したDataSetと、テーブルの内容を表現したExcelファイルとの比較(assert)も出来るらしい。テスティングフレームワークでテストを行う一番のメリットはテストの自動化なので、多少手間がかかってもそうすべきかもしれない(このあたりはプロジェクトによりけりだろう)。

今回書いたソース

EmployeeTest(テストクラス)
public class EmployeeTest extends S2TestCase {

	private static final String TEST_DIR = "c:/aaa/bbb/";

	private JdbcManager jdbcManager;

	private SqlReader reader;

	public void setUp() {
		include("test.dicon");
	}

	public void testInsertTx() {

		Department department = new Department();
		department.departmentName = "Dept.A";
		jdbcManager.insert(department).execute();

		Employee employee = new Employee();
		employee.departmentId	= department.departmentId;
		employee.employeeName	= "Emp.A";
		employee.birthDay		= new Date();
		jdbcManager.insert(employee).execute();

		XlsWriter writer = new XlsWriter(new File(TEST_DIR + "testInsert.xls"));
		reader.addTable("Department");
		reader.addTable("Employee");
		writer.write(reader.read());
	}
}
test.dicon(includeしているdiconファイル)
<components>
	<include path="app.dicon" />
	<component class="org.seasar.extension.dataset.impl.SqlReader" />
</components>

SqlReaderをdiconで定義しているのは、SqlReaderの生成にDataSourceインターフェースが必要なため。DataSourceはjdbc.diconに定義してあるので、こうして単にコンポーネントの定義だけ書いておけば、勝手にインジェクションしてくれる。

Employee(エンティティ)
@Entity
public class Employee {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public Integer employeeId;

	public Integer departmentId;
//	public Integer departmentDepartmentId;

	public String employeeName;

	@Temporal(TemporalType.DATE)
	public Date birthDay;

	@ManyToOne
	@JoinColumn(name = "DEPARTMENT_ID")
	public Department department;
}

@ManyToOneを指定する場合、自身に外部キーを格納するプロパティ(今回はdepartmentId)を定義しておかなければならないのだが、これはデフォルト(@JoinColumnの指定なし)では、"{テーブル名}_{主キー}"というプロパティとみなされるらしい。今回、部署テーブルの主キーはDEPARTMENT_IDとしている為、デフォルトで動作させようと思ったら、departmentDepartmentIdというプロパティにしなければならないようだ。これを間違えていると以下のようなエラーが出るので注意。

org.seasar.extension.jdbc.exception.ManyToOneFKNotFoundRuntimeException: [ESSR0729]エンティティ(Employees)のプロパティ(departments)が不正です。JoinColumnのnameで指定されている外部キー(DEPARTMENTS_DEPARTMENT_ID)が存在しません。
Department(エンティティ)
@Entity
public class Department {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public Integer departmentId;

	public String departmentName;

	@OneToMany(mappedBy = "department")
	public List<Employee> employeeList;
}
補足

今回は、jdbcManager.insert()をした後、正しくテーブルが更新されているかを確認しているが、実際にこうしたテストはほとんど意味が無い。挿入系で単体テストをやるとしたら、複数のテーブルにレコードを同時に作成するServiceクラスに対するテストなどに限られるだろう。