渡邊です。こんにちは。
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); }