Node.jsのAzure FunctionsとAzurite を使用して自動テストを実行する
ゴール
今回のゴールは、Github Actionsでテストを回すことです。
テストはJestで書いており、Blobへの保存などのテストはAzuriteを使います。
AzuriteはDockerで動かして、Github Actionsで起動するコンテナー内にイメージを展開します。
Docs
今回も参考にしたのは公式のドキュメントです。
https://learn.microsoft.com/ja-jp/azure/storage/blobs/use-azurite-to-run-automated-tests
こちらをNode.js(TypeScript)でやるとどうなるかという内容を書いていきます。
ざっくり解説
まず、ソースはこちらにあります。
https://github.com/xiaotiantakumi/azurite-local-sample
テンプレートは例のごとく、自分で用意しているFunctionsのテンプレートを使用しています。
https://github.com/xiaotiantakumi/az-func-ts-starter
公式ドキュメントはPythonでサンプルが書かれています。
今回、TypeScriptでソースコードを記述しました。さらに、公式には記載されていないファイルの確認、アップロード、削除をテストするコードも書いています。公式で提供されていたのは、コンテナが存在するかの確認のみでした。また、GitHub Actionsではなく、Azure Pipelineだったので自分がやりたいこととは少し違いました。というわけで、少しというか、色々書き換えています。
全体の構成
(base) takumi@ ~/Documents/src/private/azure/azurite-local-sample/SampleTrigger$ tree
.
├── blobHelper.ts
├── function.json
└── index.ts
blobHelper.ts
: Azure Blob Storageとのやり取りを助ける関数群を定義しています。ファイルのダウンロード、アップロード、削除などの操作を行う関数が含まれています。function.json
: Azure Functionsの設定ファイルで、トリガー、バインディング、その他の関数の設定を定義します。このファイルによって、関数がどのようにトリガーされ、どのように動作するかが決定されます。index.ts
: Azure Functionsのエントリーポイントとなっています。具体的な関数のロジックや、blobHelper.ts
からの関数のインポートなどが含まれています。
blobHelper.tsについて
関数の概要
getBlobClient
: 指定されたコンテナとファイル名に対応するBlobクライアントを取得します。コンテナが存在しない場合はエラーを返します。getExistingBlobClient
:getBlobClient
を使用してBlobクライアントを取得し、ファイルが存在するか確認します。ファイルが存在しない場合はエラーを返します。downloadStorageItemWithBuffer
: 指定されたコンテナとファイル名からファイルをダウンロードし、バッファとして返します。uploadStorageItemWithBuffer
: 指定されたコンテナとファイル名にバッファをアップロードします。成功時には成功メッセージを返します。deleteStorageItem
: 指定されたコンテナとファイル名のファイルを削除します。成功時には成功メッセージを返します。
ポイント
- index.tsから呼び出される: どこから呼び出されてもいいようにHelper化してますが、今回はindex.tsから呼び出されます。
- 環境変数の使用: ストレージ接続文字列は環境変数から取得されます。local.setting.jsonで書いてます。以下が今回のものになります。
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsStorage": "",
"STORAGE_CONNECTION_STRING": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;",
"DB_HOST": "localhost",
"DB_NAME": "db",
"DB_USER": "root",
"DB_PASSWORD": "root"
}
}
ところで、STORAGE_CONNECTION_STRINGはAzurite の既定値であり、秘密キーではありません。ということが以下にも書かれているので安心して下さい。
ところで、STORAGE_CONNECTION_STRINGはAzurite の既定値であり、秘密キーではありません。ということが以下にも書かれているので安心して下さい。
https://learn.microsoft.com/ja-jp/azure/storage/blobs/use-azurite-to-run-automated-tests
このコードは、Azure FunctionsとAzure Blob Storageを使用したアプリケーションで、ファイルのCRUD操作を効率的に行うためのヘルパー関数を提供しています。
Github Actionsについて
今回、StorageアカウントのテストをするためにAzuriteの部分に着目して欲しいのですが、まずは全体の流れをソースで見ていきます。
# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action
# More GitHub Actions for Azure: https://github.com/Azure/actions
name: Build and deploy Node.js project to Azure Function App - func-starter-sample
on:
push:
workflow_dispatch:
env:
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root
NODE_VERSION: '18.x' # set this to the node version to use (supports 8.x, 10.x, 12.x)
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@v2
- name: Setup Node ${{ env.NODE_VERSION }} Environment
uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VERSION }}
- name: 'Resolve Project Dependencies Using Npm'
shell: bash
run: |
pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
npm install
npm run build --if-present
popd
- name: 'Run Docker Compose'
run: |
docker compose -f ./docker-compose.yml up -d --wait
- name: 'Show Docker Compose log'
run: |
pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
docker compose logs
popd
- name: 'Wait for DB to be ready'
run: |
host="localhost"
dbUser="root"
dbPass="root"
until mysql --host=$host --user=$dbUser --password=$dbPass --protocol=TCP -e "SELECT 1"; do
echo "Waiting for DB to start..."
sleep 5
done
- name: 'Run tests'
run: |
pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
npm run test --if-present
popd
# これは、Azure Functionsのリソースが作成されていることが前提
# - name: 'Run Azure Functions Action'
# uses: Azure/functions-action@v1
# id: fa
# with:
# app-name: 'func-starter-sample'
# slot-name: 'Production'
# package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}
# # set the secret key to your Azure function.
# # you can get the secret key after setting cicd in your Azure function.
# # once you set the cicd, you can get the secret key in Github Setting pages.
# publish-profile: ${{ secrets.xxx }}
ジョブ
- リポジトリのチェックアウト: ソースコードをGitHubランナーにチェックアウトします。
- Node.jsのセットアップ: 指定されたバージョンのNode.js環境をセットアップします。
- プロジェクト依存関係の解決:
npm install
とnpm run build
を使用して、プロジェクトの依存関係を解決し、ビルドします。 - Docker Composeの実行:
docker-compose.yml
ファイルを使用してDockerコンテナを起動します。 - Docker Composeログの表示: Dockerコンテナのログを表示します。
- DBの準備待ち: MySQLデータベースが起動するのを待ちます。
- テストの実行:
npm run test
を使用してプロジェクトのテストを実行します。 - Azure Functions Actionの実行: (コメントアウトされていますが)Azure Functionsへのデプロイを行うステップ。リソースが既に作成されている必要があります。
注意点
- 最後のステップはコメントアウトしているため、現在はAzureへのデプロイは行われません。
- データベースの認証情報(ユーザー名とパスワード)がハードコードしているため、テスト目的なら問題ないはずですが、実際に流用するとなった時などにセキュリティ上の懸念があります。
このワークフローは、プロジェクトのビルド、テスト、Dockerコンテナの管理など、CI/CDパイプラインの一般的なタスクをカバーしています。最後のデプロイステップを有効にすると、Azure Functionsへの自動デプロイも可能になります。(Azureだとデプロイセンターでソースコード連携の設定してトークンを取得したりすることで可能になります。)
Github Actionsで呼び出すdocker-compose.ymlについて
まずはソースから
docker-compose.yml
version: "3"
services:
db:
image: mysql:latest
container_name: mysql_db_container
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: db
TZ: "Asia/Tokyo"
volumes:
- ./db/my.cnf:/etc/mysql/conf.d/my.cnf
- ./db/init:/docker-entrypoint-initdb.d
ports:
- "3306:3306"
azurite:
image: mcr.microsoft.com/azure-storage/azurite
ports:
- "10000:10000"
- "10001:10001"
- "10002:10002"
volumes:
- ./data:/data
今回ポイントとなるのは、azuriteのイメージです。
image: mcr.microsoft.com/azure-storage/azurite
- AzuriteのDockerイメージを指定します。このイメージは、Azure Storageのエミュレーションを提供します。
ports
- Azuriteが使用するポートをホストとコンテナ間でマッピングします。
"10000:10000"
: Blobサービスのポート"10001:10001"
: Queueサービスのポート"10002:10002"
: Tableサービスのポート
volumes
- ホストとコンテナ間でデータボリュームを共有します。
./data:/data
: ホストマシンの./data
ディレクトリをコンテナの/data
ディレクトリにマッピングします。Azuriteがデータを永続化するために使用されます。
テストについて
blobHelper.tsのテストは以下です。
import { mock } from "jest-mock-extended";
import { Context } from "@azure/functions";
import {
deleteStorageItem,
downloadStorageItemWithBuffer,
uploadStorageItemWithBuffer,
} from "../../SampleTrigger/blobHelper";
beforeAll(() => {
jest.clearAllMocks();
});
const containerName = "container1";
const deleteContainerName = "container2";
test("ファイルダウンロード失敗 - ファイルが存在しない", async () => {
const context = mock<Context>();
const fileName = "nonexistent-file.txt";
const buffer = await downloadStorageItemWithBuffer(
context,
containerName,
fileName
);
expect(buffer).toBeUndefined();
expect(context.res?.status).toBe(404);
});
test("ファイルアップロード失敗 - コンテナが存在しない", async () => {
const context = mock<Context>();
const fileName = "sample-file.txt";
const buffer = Buffer.from("Hello, World!");
await uploadStorageItemWithBuffer(
context,
"nonexistent-container",
fileName,
buffer
);
expect(context.res?.status).toBe(500);
});
test("ファイル削除失敗 - ファイルが存在しない", async () => {
const context = mock<Context>();
const fileName = "nonexistent-file.txt";
await deleteStorageItem(context, containerName, fileName);
expect(context.res?.status).toBe(404);
});
test("ファイルダウンロード成功", async () => {
const context = mock<Context>();
const fileName = "sample.txt";
const buffer = await downloadStorageItemWithBuffer(
context,
containerName,
fileName
);
expect(buffer).toBeDefined();
});
test("ファイルアップロード成功", async () => {
const context = mock<Context>();
const fileName = "sample-file.txt";
const buffer = Buffer.from("Hello, World!");
await uploadStorageItemWithBuffer(context, containerName, fileName, buffer);
expect(context.res?.status).toBe(200);
});
test("ファイル削除成功", async () => {
const context = mock<Context>();
const fileName = "sample-file.txt";
const buffer = Buffer.from("Hello, World!");
await uploadStorageItemWithBuffer(
context,
deleteContainerName,
fileName,
buffer
);
console.log("uploadStorageItemWithBuffer", context.res);
const checkExist = await downloadStorageItemWithBuffer(
context,
deleteContainerName,
fileName
);
if (!checkExist) {
throw new Error("ファイルが存在しません");
}
await deleteStorageItem(context, deleteContainerName, fileName);
expect(context.res?.status).toBe(200);
});
上記のテストコードでは、実際にAzuriteを用いてAzure Blob Storageの操作をテストしています。Azuriteは、Azure Storageサービスのエミュレータで、ローカル開発環境でAzure Blob Storage、Queue Storage、Table Storageをエミュレートすることができます。(今回はAzure Blob Storageしか使ってないですが)
以下は、このテストコードのポイントと、Azuriteを使用する利点です。
ポイント
- 実際の操作のテスト: モックではなくAzuriteを使用することで、ファイルのダウンロード、アップロード、削除などの実際の操作をテストすることができます。
- 成功と失敗の両方のシナリオ: ファイルやコンテナが存在しないなどのエラーケースと、正常な操作の成功ケースの両方をカバーしています。
- 状態の検証: 各テストでは、操作後のバッファの状態やHTTPレスポンスステータスなどを検証しています。
Azuriteを使用する利点
- 開発環境の整合性: Azure Storageの実際の環境と同じように動作するため、開発とテストの間で整合性を保つことができます。
- コスト削減: ローカルでエミュレートするため、クラウドリソースのコストを削減できます。
- オフライン開発: インターネット接続がない場所でも開発とテストが可能です。
ディスカッション
コメント一覧
まだ、コメントがありません