GitHub ActionsでAWS Step Functions LocalとJestによるステートマシンのMockテストを実行する

2022.07.29

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

前回の下記エントリでは、AWS Step Functions LocalによるMockテストをJestで実行してみました。

今回は、同じくAWS Step Functions LocalとJestによるState MachineのMockテストをGitHub Actionsで実行してみました。

やってみた

実装のファイル構成は次のようになります。

$ tree
.
├── .github
│   └── workflows
│       └── ci.yml
├── jest.config.js
├── package-lock.json
├── package.json
├── test
│   └── mock
│       └── stepfunctions
│           ├── MyStateMachine
│           │   ├── MockConfigFile.json
│           │   ├── MyStateMachine.asl.json
│           │   └── MyStateMachine.test.ts
│           └── docker-compose.yml
└── tsconfig.json

導入しているパッケージです。

$ npm ls --depth=0
project@0.1.0 /path/to/project
├── @types/jest@28.1.6
├── @types/node@18.6.2
├── jest@28.1.3
├── ts-jest@28.0.7
├── ts-node@10.9.1
└── typescript@4.7.4

Test files

test/mock/stepfunctions/MyStateMachine配下ではテスト内容に関するファイルを管理しています。

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

test/mock/stepfunctions/MyStateMachine/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
          }
        }
      }
    }
  }
}

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

test/mock/stepfunctions/MyStateMachine/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
    }
  }
}

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

test/mock/stepfunctions/MyStateMachine/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,
  credentials: { accessKeyId: 'dummy', secretAccessKey: 'dummy' },
  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');
  });
});

Docker Compose file

AWS Step Functions LocalのコンテナImageを実行するためのDocker Composeの構成ファイルです。実行時にMockConfigFile.jsonをマウントします。

test/mock/stepfunctions/docker-compose.yml

version: '3'
services:
  sfn_local:
    image: amazon/aws-stepfunctions-local
    volumes:
      - ./MyStateMachine/MockConfigFile.json:/home/StepFunctionsLocal/MockConfigFile.json
    environment:
      SFN_MOCK_CONFIG: /home/StepFunctionsLocal/MockConfigFile.json
    ports:
      - 8083:8083

Workflow file

GitHub ActionsのWorkflowのファイルです。docker composeコマンドでAWS Step Functions Local環境を立ち上げ、Jestコマンドでテストを実行しています。

.github/workflows/ci.yml

name: CI
on: workflow_dispatch

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Cache CDK Dependency
        uses: actions/cache@v3
        id: cache_cdk_dependency_id
        env:
          cache-name: cache-cdk-dependency
        with:
          path: node_modules
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}
          restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-

      - name: Clean install Dependency
        if: ${{ steps.cache_cdk_dependency_id.outputs.cache-hit != 'true' }}
        run: npm ci

      - name: Run container image
        working-directory: test/mock/stepfunctions
        run: docker compose up -d

      - name: Run Step Functions mock test
        run: npx jest test/mock/stepfunctions

動作確認

Workflowを実行すると、テストが実行されSuccessしました。

Run npx jest test/mock/stepfunctions
  npx jest test/mock/stepfunctions
  shell: /usr/bin/bash -e {0}
  
PASS test/mock/stepfunctions/MyStateMachine/MyStateMachine.test.ts (10.315 s)
  MyStateMachine
    ✓ FugaPathTest (1180 ms)
    ✓ NotFugaPathTest (1046 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        10.426 s
Ran all test suites matching /test\/mock\/stepfunctions/i.

おわりに

GitHub ActionsでAWS Step Functions LocalとJestによるステートマシンのMockテストを実行してみました。

これでコードのPush時などにStep FunctionsのMockテストをCI Workflowの一環として実行できるようになりました。次回はさらにAWS CDKで開発をしている場合にASLを自動でテストに使用できるようにしたいです。

参考

以上