SAP Workflow Serviceのテストを自動化する

この記事は [chillSAP 夏の自由研究2021](https://note.com/chillsap/n/n08ef5ec73b4c) の記事として執筆しています。

#1. はじめに
最近、[SAP Workflow ServiceのPoC](https://qiita.com/tami/items/79e2b0bf4e0b74239d98)と称してワークフローを触っています。開発の中で、次のようなことを課題と感じていました。

ワークフローの中にはスクリプトタスクと呼ばれるタスクを配置することができ、その中でJavaScriptを使ってコンテキスト（ワークフローで持っているデータ）を編集することができます。ユーザータスクの前後でコンテキストを編集し、その結果で分岐することで、ワークフローに様々な動作をさせることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/911e615e-a687-4843-7b23-6d3a6b7e4be5.png)
しかし、スクリプトタスクのエディタは簡素なもので、たとえば存在しないコンテキストのプロパティを参照したとしても開発時にエラーは出ません。また、JavaScriptの比較的新しい構文（たとえば[Array.prototype.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find)）は使えないなどの制約もあります。これらは、デプロイして動かしたときにはじめてエラーになります。さらに、コンテキストの同じプロパティを複数個所で使用していると、ある個所を変更した影響で他の箇所でエラーになる場合があります。このため、スクリプトタスクの変更はけっこう神経を使います。

スクリプトを変えてデプロイし、すべての分岐パターンをテスト、を毎回マニュアルでやるのはかなり手間です。そこで、必要なテストパターンを網羅したリクエストをWorkflow Serviceに投げてくれるAPIを作り、テストを自動化してみようと思いました。

#2. テストの自動化
少し前にSAP Cloud SDKのことを調べていて、[SAP Workflow Service用のAPIクライアント](https://sap.github.io/cloud-sdk/docs/js/features/openapi/pregenerated-clients/workflow)があることを知りました。APIクライアントの使い方については、以下の記事を書いています。
[SAP Cloud SDK for JavaScript – Getting started with Workflow API Client](https://blogs.sap.com/2021/07/03/sap-cloud-sdk-for-javascript-getting-started-with-workflow-api-client/)

今回は、上記の記事で作成した[プロジェクト](https://github.com/miyasuta/wf_cloud_sdk)をベースに、テスト用のリクエストを投げるためのエンドポイントを追加します。プロジェクトの構成は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8506e8b8-76f2-a7c5-58a1-fc94f8b5ab11.png)

##2.1. テストのインプット
テストの際は、以下のようなリクエストを投げます。`cases`という配列の中に、各テストケースで使うパラメータが入っています。

|項目  |説明  |
|---|---|
|id  |テストケースを示す番号  |
|description  |テストケースの説明  |
|context  |ワークフロー開始時に渡すコンテキスト  |
|decisions  |ユーザータスクの完了に渡すコンテキスト  |

```json
{
	"cases": [{
            "id": 1,
            "description": "straight approval",
			"context": {
				"approvalSteps": [{
						"userId": "mio.fujita.01@gmail.com",
						"taskType": "APPROVAL"
					},
					{
						"userId": "mio.fujita.01@gmail.com",
						"taskType": "APPROVAL"
					}
				]
			},
			"decisions": [{
					"decision": "approve"
				},
				{
					"decision": "approve"
				}
			]
		},
		{
            "id": 2,
            ...
		}
	]
}
```

##2.2. テスト用のエンドポイント
`wftest.controller.ts`にテスト用のリクエストを受ける処理を書きます。

```javascript
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
import { WorkflowService } from '../workflow/workflow.service';

@Controller('wftest')
export class WftestController {
  constructor(private readonly workflowService: WorkflowService) {}

  @Post('/automate')
  @HttpCode(201)
  async automaticTest(@Body() body): Promise<any> {
    const cases = body.cases;
    const results = [];
    let index;
    let workflowInstance;
    let workflowResult;
    
    //caseの数だけ繰り返し
    try {
      for (index = 0; index < cases.length; index++) {
        const context = cases[index].context;
        const decisions = cases[index].decisions;

        //workflow開始
        // eslint-disable-next-line prefer-const
        workflowInstance = await this.workflowService.startWorkflow(
          'multilevelapproval',
          context,
        );

        //decisionsの回数だけ繰り返し、ユーザータスクを取得
        for (let j = 0; j < decisions.length; j++) {
          //get active task
          await this._sleep(2000);
          // eslint-disable-next-line prefer-const
          let userTask = await this.workflowService.getActiveTask(
            workflowInstance.id,
          );
          //ユーザータスクを完了させる
          await this.workflowService.updateUserTask(userTask.id, decisions[j]);
        }

        //ワークフローのステータスをチェック
        //全て完了した時点でCOMPLETEDになっているはず
        await this._sleep(2000);
        // eslint-disable-next-line prefer-const
        workflowResult = await this.workflowService.getWorkflowInstance(
          workflowInstance.id,
        );

        //結果を格納
        results.push({
          id: cases[index].id,
          description: cases[index].description,
          workflowInstanceId: workflowInstance.id,
          workflowStatus: workflowResult.status,
          result: workflowResult.status === 'COMPLETED' ? 'SUCCESS' : 'ERROR',
        });
      }
    } catch (err) {
      //エラーが起きた場合、現在のステータスを取得して結果に設定
      workflowResult = await this.workflowService.getWorkflowInstance(
        workflowInstance.id,
      );
      results.push({
        id: cases[index].id,
        description: cases[index].description,
        workflowInstanceId: workflowInstance?.id,
        workflowStatus: workflowResult.status,
        result: 'ERROR',
        message: err.message,
      });
    }
    return results;
  }

  private _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
}
```
ワークフローのAPIを呼ぶ前に`await this._sleep(2000)`でwaitを入れていますが、これはワークフローが非同期に更新されるので、しばらく待たないとユーザータスクができていなかったり、ステータスが想定のものに更新されていなかったりするためです。現状2秒のwaitでうまくいっていますが、場合によってはwait時間を増やす必要があります。

##2.3. テストの実行
プロジェクトをCloud Foundryにデプロイ後、Postmanからテストを実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/261e95b4-c79c-5ce7-21ee-80142492c82f.png)
正常終了した場合、以下のようにレスポンスが返ってきます。waitを入れているため、ケースが増えると実行時間がかなり長くなります。その場合、リクエストを分割したほうがよいでしょう。

```
[
    {
        "id": 1,
        "description": "straight approval",
        "workflowInstanceId": "142a0253-f105-11eb-a71e-eeee0a99bbe4",
        "workflowStatus": "COMPLETED",
        "result": "SUCCESS"
    }
]
```

#3. おわりに
テストを自動化したことにより、ワークフローを変更したときにすぐに全パターンのテストができるようになり、スクリプトタスクの変更が気軽にできるようになりました。自動化の仕組みを作るのは最初は少し手間ですが、複雑なパターンのワークフローを開発する場合にはメリットが上回りそうです。