渡邊です。こんにちは。
jBPM5の研究をしています。
備忘録を兼ねて、BPMN2 Processの単体テストについてまとめます。
Guvnor、TaskService、DBなどを使わずに、オフラインでBPMN2 Processの分岐網羅テストをしました。また、異なる経路を辿る複数のプロセスを並列に走らせ、整合性をテストしました(おまけ程度)。
お小遣い申請フローです。Guvnorを使わずに、JBoss Developer Studioで作成しました。図からは読み取れませんが、各User Taskの主なプロパティは次の通りです。
注意点は、タスク「No」は、Script Taskであることです(残念でしたという旨のメールが自動で送信される想定です)。
| Name | TaskName | ActorId | Parameter Mapping | Result Mapping |
|---|---|---|---|---|
| Request | RequestTask | husband | requestId=requestId | |
| Response | ResponseTask | wife | requestId=requestId | outcome=outcome |
| Receive | ReceiveTask | husband | requestId=requestId |
作成したJUnitのテストコードはこちらです。
必要なJAR
テストコードの作成、およびテストの実行も、JBoss Developer Studioを使いました。
droolsjbpm-integration-distribution-xxxあたりに同梱されたJARをやみくもにビルドパスに追加すると、JARが多すぎる為か、JUnitテストを実行できません。
必要なJARは次の通りです(もっと減らせるかもしれませんが、それは目的ではないので、この辺で)。
バージョンはよしなに読み替えてください。
- antlr-3.3.jar
- antlr-runtime-3.3.jar
- commons-beanutils-1.7.0.jar
- commons-collections-3.2.1.jar
- commons-compress-1.0.jar
- commons-digester-1.8.jar
- commons-exec-1.0.1.jar
- commons-io-1.4.jar
- commons-jexl-1.1.jar
- commons-lang-2.4.jar
- commons-logging-1.1.1.jar
- commons-logging-api-1.1.jar
- commons-net-2.0.jar
- drools-compiler-5.5.0.Final.jar
- drools-core-5.5.0.Final.jar
- ecj-3.5.1.jar
- jbpm-bpmn2-5.4.0.Final.jar
- jbpm-flow-5.4.0.Final.jar
- jbpm-flow-builder-5.4.0.Final.jar
- jbpm-human-task-core-5.4.0.Final.jar
- jbpm-human-task-hornetq-5.4.0.Final.jar
- jbpm-workitems-5.4.0.Final.jar
- knowledge-api-5.5.0.Final.jar
- knowledge-internal-api-5.5.0.Final.jar
- mvel2-2.1.3.Final.jar
- slf4j-api-1.6.4.jar
- slf4j-simple-1.6.1.jar
ちょっと脱線するので省きますが、ecj-3.5.1.jarに関連して、こちらに興味深い記事がありました。
テスト用のWorkItemHandler
WorkItemを内部のListに溜め込むだけのWorkItemHandlerです。このWorkItemHandlerを使い、プロセス内に存在するHuman Taskの数、あるHuman Task実行後における次のHuman Taskの状態を検証するテストコードを記述します。
public class MockHumanTaskWorkItemHandler implements WorkItemHandler {
/**
* executeWorkItemメソッドで受けた全てのWorkItemを保持するList
*/
private List workItems;
public MockHumanTaskWorkItemHandler() {
workItems = Collections.synchronizedList(new ArrayList());
}
@Override
public void executeWorkItem(WorkItem workItem, WorkItemManager manager) {
workItems.add(workItem);
}
@Override
public void abortWorkItem(WorkItem workItem, WorkItemManager manager) {
manager.abortWorkItem(workItem.getId());
}
/**
* @return 最後に追加されたWorkItem
*/
public WorkItem lastWorkItem() {
return workItems.get(workItems.size() - 1);
}
public int getWorkItemSize() {
return workItems.size();
}
}
セッション取得
テスト用のWorkItemHandlerをWorkItemManagerに登録します。この時に指定する文字列は、”Human Task”でなければなりません。
@BeforeClass
public static void beforeClass() {
KnowledgeBuilder builder = KnowledgeBuilderFactory.newKnowledgeBuilder();
builder.add(ResourceFactory.newClassPathResource("giveMeMoneyProcess.bpmn"), ResourceType.BPMN2);
knowledgeBase = builder.newKnowledgeBase();
}
@Before
public void before() {
workItemHandler = new MockHumanTaskWorkItemHandler();
ksession = knowledgeBase.newStatefulKnowledgeSession();
ksession.getWorkItemManager().registerWorkItemHandler("Human Task", workItemHandler);
}
プロセス開始とHuman Task完了
プロセス開始メソッド(startProcess)は最初のHuman Taskを、Human Task完了メソッド(completeWorkItem)は、次のHuman Task(終了ノードに達した場合はnull)を返すようにしています。
1経路あたりのテスト
分岐網羅を100%にする為に必要なプロセス数は、2ですが、せっかくなので3プロセスに分けてテストしました。
次のテストコードは、 お小遣いの取得に一発で成功する経路のテストです。「Receive」完了後は、Uset Taskが増えていないことを確認しています。
/**
* お小遣い取得成功テスト
*/
@Test
public void testSuccessProcess() {
doTestSuccessProcess(true);
}
/**
* お小遣い取得成功テストの本体
*/
private void doTestSuccessProcess(boolean checkWorkItemNum) {
final String requestId = "1";
WorkItem request = startProcess(requestId);
assertWorkItem(request, checkWorkItemNum, 1, "RequestTask", "husband", requestId);
WorkItem response = completeWorkItem(request, null);
assertWorkItem(response, checkWorkItemNum, 2, "ResponseTask", "wife", requestId);
WorkItem receive = completeWorkItem(response, "yes");
assertWorkItem(receive, checkWorkItemNum, 3, "ReceiveTask", "husband", requestId);
WorkItem end = completeWorkItem(receive, null);
Assert.assertNull(end);
assertWorkItemNum(checkWorkItemNum, 3);
}
並列処理
全経路テストが済んだあとで、3つのプロセスを並列で走らせ、全プロセス終了後の整合性をテストしました。といっても、この例では最終的なWorkItemの数を検証しただけです。
ものによっては、もっと面白いテストを作成できると思います。
この程度のテストで確認できることは、BRMS5の開発者が検証しているはずですが、自分自身で試すことにより、BRMS5の素晴らしさを改めて体感しました。
@Test
public void testCouponProcessMultiThread() {
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.execute(new Runnable() {
@Override
public void run() {
doTestRetryProcess(false);
}
});
pool.execute(new Runnable() {
@Override
public void run() {
doTestFailureProcess(false);
}
});
pool.execute(new Runnable() {
@Override
public void run() {
doTestSuccessProcess(false);
}
});
boolean isSuccessful = false;
try {
pool.shutdown();
isSuccessful = pool.awaitTermination(1, TimeUnit.MINUTES);
if (!isSuccessful) pool.shutdownNow();
} catch (InterruptedException e) {
pool.shutdownNow();
}
// タイムアウトしないで終了していること
Assert.assertEquals(true, isSuccessful);
// 全スレッドが正しく終了していれば、タスクの数は次の期待値であること
assertWorkItemNum(true, 10);
}