さまざまなシナリオにおける動作を確認するJUnitテスト(リスト4)を用意しました。これらの単体テストでは、複数の“setup”(@Before)メソッドを実行できる「JUnit 4.x」の機能を利用しています。この@Beforeメソッドとしては、経費報告書を初期化するcreateReport、テストで使用される金額とコードに記述された実際の制限値が一致することを確認するamountPreconditions、そして管理者(Manager)、副社長(VicePresident)、CEOをオブジェクトとして含むデフォルトの命令系統を作成するメソッドがあります。
最終テストのapprovedByPeerWhenManagerOutOfOfficeでは、Chain of Responsibilityパターンの実装が備えるべき重要な機能の1つ、すなわち命令系統の動的な変更が可能かどうかが検証されます。このテストシナリオにおいて、最初の管理者(Joe)は自分が外出して不在になることを知っています。彼は自分の所に送られてくるすべての経費報告書が自分と同位の管理者(Jim)へ転送されるように手配します。JimはJoeと同じ副社長に報告を行うことになっているため、Jimの段階でも経費報告書を処理できない場合は、その副社長を次の承認者に設定します。
import static org.junit.Assert.*;
import org.junit.*;
public class ChainTest {
private Manager manager;
private VicePresident vp;
private CEO ceo;
private ExpenseReport report;
private static final int SMALL_AMOUNT = 100;
private static final int VP_AMOUNT = 7000;
private static final int CEO_AMOUNT = 207000;
@Before
public void amountPreconditions() {
assertTrue(SMALL_AMOUNT <= manager.getDollarLimit());
assertTrue(manager.getDollarLimit() < VP_AMOUNT &&
VP_AMOUNT <= vp.getDollarLimit());
assertTrue(vp.getDollarLimit() < CEO_AMOUNT);
}
@Before
public void createReport() {
report = new ExpenseReport();
}
@Before
public void createDefaultChain() {
manager = new Manager("Joe", false);
vp = new VicePresident("Jane");
ceo = new CEO("Zeus");
manager.setNextApprover(vp);
vp.setNextApprover(ceo);
}
@Test
public void approvedByManager() {
report.setTotalDollarAmount(SMALL_AMOUNT);
manager.handle(report);
assertApprovedBy(manager);
}
@Test
public void approvedByVPWhenOverManagerLimit() {
report.setTotalDollarAmount(VP_AMOUNT);
manager.handle(report);
assertApprovedBy(vp);
}
@Test
public void approvedByCEOWhenOverVPLimit() {
report.setTotalDollarAmount(CEO_AMOUNT);
manager.handle(report);
assertApprovedBy(ceo);
}
@Test
public void approvedIntlByIntlManager() {
report.setTotalDollarAmount(SMALL_AMOUNT);
report.setIsInternationalTravel(true);
manager.setCanApproveInternational(true);
manager.handle(report);
assertApprovedBy(manager);
}
@Test
public void approvedIntlByVPWhenManagerNotIntl() {
report.setTotalDollarAmount(SMALL_AMOUNT);
report.setIsInternationalTravel(true);
assertFalse(manager.canApproveInternational());
manager.handle(report);
assertApprovedBy(vp);
}
@Test
public void approvedByVPWhenManagerOutOfOffice() {
report.setTotalDollarAmount(SMALL_AMOUNT);
manager.setOutOfOffice(true);
manager.handle(report);
assertApprovedBy(vp);
}
@Test
public void rejectedWhenCEOOutOfOffice() {
report.setTotalDollarAmount(CEO_AMOUNT);
ceo.setOutOfOffice(true);
manager.handle(report);
assertSame(State.rejected, report.state());
}
@Test
public void approvedByPeerWhenManagerOutOfOffice() {
report.setTotalDollarAmount(SMALL_AMOUNT);
Manager peer = new Manager("Jim", false);
peer.setNextApprover(vp);
manager.setOutOfOffice(true);
manager.setNextApprover(peer);
manager.handle(report);
assertApprovedBy(peer);
}
private void assertApprovedBy(Approver approver) {
assertSame(State.approved, report.state());
assertSame(approver, report.getHandler());
}
}
Chain of Responsibilityパターンを使うクライアントは、どの承認者が最終的に経費報告書を処理することになるか知らず、実際そのことを気に掛けません。この実装において、経費報告書を提出する社員は最初の承認者(直属の上司)を知っています。しかし、クライアントがこうしたことまで知らなければならない理由はありません。クライアントに対して適切なApproverの実装を生成するようなファクトリを用意することも可能です。

