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