AWS Step Functions LocalによるMockテストをJestで実行してみた

2022.07.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部 IoT事業部の若槻です。

前回のエントリでは、AWS Step Function LocalをMock Responseを使って試してみました。

AWS Step Function Localによるテストは便利なのですが、せっかくならテスト結果の評価まで含めて自動化したいですね。

そこで今回は、AWS Step Functions LocalによるMockテストをJestで実行してみました。

やってみた

AWS Step Function Local環境の準備

テスト対象のState Machine(後述)に対するMock ResponseのConfigです。これによりAWS ServiceからTaskへのレスポンスをMockすることができます。またレスポンスは複数のパターンを記述し、テスト実行時に使用することが可能です。

MockConfigFile.json

{
  "StateMachines": {
    "MyStateMachine": {
      "TestCases": {
        "FugaPathTest": {
          "GetParameter": "GetParameterFugaMockedSuccess"
        },
        "NotFugaPathTest": {
          "GetParameter": "GetParameterNotFugaMockedSuccess"
        }
      }
    }
  },
  "MockedResponses": {
    "GetParameterFugaMockedSuccess": {
      "0": {
        "Return": {
          "Parameter": {
            "Arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/hoge",
            "DataType": "text",
            "LastModifiedDate": "2022-07-26T05:38:43.052Z",
            "Name": "hoge",
            "Type": "String",
            "Value": "fuga",
            "Version": 4
          }
        }
      }
    },
    "GetParameterNotFugaMockedSuccess": {
      "0": {
        "Return": {
          "Parameter": {
            "Arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/hoge",
            "DataType": "text",
            "LastModifiedDate": "2022-07-26T05:38:43.052Z",
            "Name": "hoge",
            "Type": "String",
            "Value": "nyao",
            "Version": 4
          }
        }
      }
    }
  }
}

AWS Step Function LocalのDockerイメージamazon/aws-stepfunctions-localをPullします。

docker pull amazon/aws-stepfunctions-local

Pullしたイメージからコンテナを実行します。

docker run -p 8083:8083 \
  --mount type=bind,readonly,source=$(pwd)/MockConfigFile.json,destination=/home/StepFunctionsLocal/MockConfigFile.json \
  -e SFN_MOCK_CONFIG="/home/StepFunctionsLocal/MockConfigFile.json" \
  amazon/aws-stepfunctions-local

Jestによるテスト実行

テスト対象となるState MachineのASL(Amazon States Language)定義のファイルです。

MyStateMachine.asl.json

{
  "Comment": "A description of my state machine",
  "StartAt": "GetParameter",
  "States": {
    "GetParameter": {
      "Type": "Task",
      "Next": "Choice",
      "Parameters": {
        "Name": "hoge"
      },
      "Resource": "arn:aws:states:::aws-sdk:ssm:getParameter",
      "ResultSelector": {
        "hoge.$": "$.Parameter.Value"
      }
    },
    "Choice": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.hoge",
          "StringMatches": "fuga",
          "Next": "FugaPath"
        }
      ],
      "Default": "NotFugaPath"
    },
    "FugaPath": {
      "Type": "Pass",
      "End": true
    },
    "NotFugaPath": {
      "Type": "Pass",
      "End": true
    }
  }
}

AWS Systems Manager Parameter StoreからGet:Parameterで値を取得し、値の文字列によりChoice Stateでパスの分岐を行います。

Jestのテストコードです。上述のASLファイルを使用してコンテナ上にState Machieを作成して実行し、実行が成功していることおよびMock Responseで定義したテストケース毎に想定したパスを経由していることを確認しています。

MyStateMachine.test.ts

import {
  SFNClient,
  GetExecutionHistoryCommand,
  StartExecutionCommand,
  CreateStateMachineCommand,
  DescribeExecutionCommand,
  ExecutionStatus,
  HistoryEvent,
  DeleteStateMachineCommand,
} from '@aws-sdk/client-sfn';
import TEST_TARGET_ASL = require('./MyStateMachine.asl.json');

const DUMMY_AWS_REGION = 'us-east-1';
const DUMMY_AWS_ACCOUNT = '123456789012';
const STEP_FUNCTIONS_LOCAL_ENDPOINT = 'http://localhost:8083';
const DUMMY_EXECUTION_ROLE_ARN = `arn:aws:iam::${DUMMY_AWS_ACCOUNT}:role/DummyRole`;
const STATE_MACHINE_NAME = 'MyStateMachine';
const DUMMY_STATE_MACHINE_ARN = `arn:aws:states:${DUMMY_AWS_REGION}:${DUMMY_AWS_ACCOUNT}:stateMachine:${STATE_MACHINE_NAME}`;

const sfnClient = new SFNClient({
  region: DUMMY_AWS_REGION,
  endpoint: STEP_FUNCTIONS_LOCAL_ENDPOINT, //AWS Step Functions Localのエンドポイントを指定
});

//State Machineの作成
beforeAll(async () => {
  await sfnClient.send(
    new CreateStateMachineCommand({
      name: STATE_MACHINE_NAME,
      roleArn: DUMMY_EXECUTION_ROLE_ARN,
      definition: JSON.stringify(TEST_TARGET_ASL),
    }),
  );
});

//State Machineの削除
afterAll(async () => {
  await sfnClient.send(
    new DeleteStateMachineCommand({
      stateMachineArn: DUMMY_STATE_MACHINE_ARN,
    }),
  );
});

//State Machine実行の開始および履歴取得
const startExecutionAndGetExecutionHistory = async (
  testCase: string,
): Promise<HistoryEvent[]> => {
  const executionResult = await sfnClient.send(
    new StartExecutionCommand({
      name: testCase,
      stateMachineArn: `${DUMMY_STATE_MACHINE_ARN}#${testCase}`,
    }),
  );
  const executionArn = executionResult.executionArn;

  let executionStatus = 'RUNNING';
  while (executionStatus === 'RUNNING') {
    await new Promise((r) => setTimeout(r, 1000));

    const res = await sfnClient.send(
      new DescribeExecutionCommand({
        executionArn: executionArn,
      }),
    );

    if (res.status !== 'RUNNING')
      executionStatus = res.status as ExecutionStatus;
  }

  const executionHistory = await sfnClient.send(
    new GetExecutionHistoryCommand({
      executionArn: executionArn,
      includeExecutionData: true,
    }),
  );

  return executionHistory.events as HistoryEvent[];
};

describe('MyStateMachine', () => {
  test('FugaPathTest', async () => {
    const executionHistory = await startExecutionAndGetExecutionHistory(
      'FugaPathTest',
    );

    //State Machine実行の分岐がFugaPathを経由していることを確認
    expect(executionHistory[8].stateEnteredEventDetails?.name).toBe('FugaPath');
    //State Machine実行が成功していることを確認
    expect(executionHistory[10].type).toBe('ExecutionSucceeded');
  });

  test('NotFugaPathTest', async () => {
    const executionHistory = await startExecutionAndGetExecutionHistory(
      'NotFugaPathTest',
    );

    //State Machine実行の分岐がNotFugaPathを経由していることを確認
    expect(executionHistory[8].stateEnteredEventDetails?.name).toBe(
      'NotFugaPath',
    );
    //State Machine実行が成功していることを確認
    expect(executionHistory[10].type).toBe('ExecutionSucceeded');
  });
});

ASLをJsonファイルからImportするので、tsconfig.jsoncompilerOptions.resolveJsonModuletrueに設定します。

tsconfig.json

{
  "compilerOptions": {
    "resolveJsonModule": true
  }
}

Jestでテストを実行すると、Successしました。

$ npx jest MyStateMachine.test.ts
 PASS  ./MyStateMachine.test.ts (5.376 s)
  MyStateMachine
    ✓ FugaPathTest (1054 ms)
    ✓ NotFugaPathTest (1043 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        5.413 s
Ran all test suites matching /MyStateMachine.test.ts/i.

おわりに

AWS Step Functions LocalによるMockテストをJestで実行してみました。

ドキュメントで紹介されているテスト方法はAWS CLIを使う方法のみでしたが、AWS SDKからもAWS Step Functions Localを使用できてよかったです。

参考

以上