[Rust] Miniflareでテストを実行 [Cloudflare Workers]
Introduction
MiniflareはCloudflare Workersをテストするためのシミュレータです。
KV/D1/R2などの他のサービスのモックも提供してくれるので、
Workersの単体テストを書くのに重宝します。
今回はMiniflareを使ってCloudflare Workers(Rust)のテストをしてみます。
Miniflare?
↑でもいったとおり、MiniflareはCloudflare Workers用のシミュレータです。
本番のCloudflare Workersと同じ、
workerdというJavaScript/Wasm Runtimeで実行されてます。
詳細なトレースログがみれたり、KVやDurable Objects、D1など
ほとんどのWorkers機能をサポートしてます。
動作も軽く、APIから簡単に起動できるので楽という利点があります。
※現行のMiniflare v3はAPIのみで、CLIは使えません
そんなMiniflareですが、ここでも言及されているとおり情報が少ないです。
現在Miniflareはversion 3なのですが、古い情報しかなかったりするので、
場合によってはDiscordやGithubで情報を探していろいろ確認する必要があります。
Wranglerがあるけども
WranglerはCloudflare Workers用のCLIツールです。
Workersのプロジェクト作成からビルド・ローカル実行・デプロイなどの管理を
すべて行うことが可能になっています。
(WranglerはMiniflareを統合している)
WranglerがあるのにMiniflareを単体で使用する意味があるのか、というところですが、
Wranglerより高速だったり詳細な設定ができたり、
各サービスのBindingも簡単に取得できたりするのでメリットはあります。
Environment
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 13.5.2
- Linux VM : ubuntu23(OrbStack使用)
- Rust : 1.75.0
M1 MacだとMiniflareが動かなかったので
OrbStackのLinuxで動かしてます。
Try
ではまず、シンプルなCloudflare Workersを作成して
Miniflareで動かしてみます。
WranglerでWorkersの雛形を作成します。
% npx wrangler generate hello-world-rust https://github.com/cloudflare/workers-sdk/templates/experimental/worker-rust % cd hello-world-rust
wrangler.tomlに変数を定義します。
[vars] KEY = "Miniflare"
src/lib.rsに処理を書きます。
use worker::*; #[event(fetch, respond_with_errors)] async fn main(req: Request, env: Env, ctx: Context) -> Result<Response> { console_log!("Cloudflare Workers"); let value:String = env.var("KEY")?.to_string(); let resp:String = format!("Hello {}",value); Response::ok(resp) }
ローカルで起動して動作確認してみます。
「Hello Miniflare」とレスポンスが返ってくればOK。
% npx wrangler dev ・ ・
起動せずビルドだけしたい場合は↓でもOKです。
% npx wrangler deploy --dry-run
次にMiniflareモジュールをインストーします。
% npm install --save-dev miniflare
test.mjsファイルを作成し、miniflareモジュールに
さきほどのWorkersを指定します。
bindingsで変数(wrangler.tomlに書くやつ)の指定も可能です。
import assert from "node:assert"; import { Miniflare } from "miniflare"; const mf = new Miniflare({ scriptPath: "./hello-world-rust/build/worker/shim.mjs", modules: true, modulesRules: [ { type: "CompiledWasm", include: ["**/*.wasm"], fallthrough: true } ], verbose:true, bindings: { KEY: "Miniflare" }, }); const res = await mf.dispatchFetch("http://localhost"); assert.strictEqual(await res.text(), "Hello Miniflare"); await mf.dispose();
nodeで実行。動いてます。
% node test.mjs workerd/io/worker.c++:1627: info: console.log(); message() = ["Cloudflare Workers"]
D1のテスト
次はD1のテストをMiniflareでやってみます。
ここのコードを参考に、
Workersを修正します。
use worker::*; use serde::{Serialize,Deserialize}; use serde_json::from_str; #[derive(Serialize,Deserialize,Debug)] struct Users { id: u32, name: String, } #[event(fetch, respond_with_errors)] pub async fn main(request: Request, env: Env, _ctx: Context) -> Result<Response> { Router::new() .get_async("/", |_, ctx| async move { //get all users let d1 = ctx.env.d1("DB")?; let statement = d1.prepare("select * from users"); let result = statement.all().await?; Response::from_json(&result.results::<Users>().unwrap()) }) .get_async("/:id", |_, ctx| async move { //get user by id let id = ctx.param("id").unwrap(); let d1 = ctx.env.d1("DB")?; let statement = d1.prepare("select * from users where id = ?"); let query = statement.bind(&[id.into()])?; let result = query.first::<Users>(None).await?; match result { Some(user) => Response::from_json(&user), None => Response::error("Not found", 404), } }) .run(request, env) .await }
ビルドします。
% npx wrangler deploy --dry-run --outdir=dist
テストはavaを使って書いてみます。
これ以外にもjestやvitestとかも使えるようです。
普通にnpm i ava だと動かなかったので↓のバージョンをインストール。
% npm install ava@1.0.0-beta.4
test/index.spec.jsファイルを作成し、
↓のようにテストを書いてみます。
import test from "ava"; import { Miniflare } from "miniflare"; test.beforeEach((t) => { const mf = new Miniflare({ scriptPath: "./hello-world-rust/build/worker/shim.mjs", modules: true, modulesRules: [ { type: "CompiledWasm", include: ["**/*.wasm"], fallthrough: true } ], verbose:true, bindings: { KEY1: "value1", KEY2: "value2", }, d1Databases: ['DB'], }); t.context = { mf }; }); test("select users", async (t) => { // Get the Miniflare instance const { mf } = t.context; //create schema & insert data const d1 = await mf.getD1Database('DB'); await d1.exec('DROP TABLE IF EXISTS users;'); await d1.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);'); await d1.exec(`INSERT INTO users (id, name) VALUES (1, 'Taro');`); await d1.exec(`INSERT INTO users (id, name) VALUES (2, 'Hanako');`); // Dispatch a fetch event to our worker const result_json = [{ id: 1, name: 'Taro' }, { id: 2, name: 'Hanako' } ]; const res = await mf.dispatchFetch("http://localhost/"); t.deepEqual(await res.json(), result_json); });
npxで実行してみます。
% npx ava@1.0.0-beta.4 --verbose test/index.spec.js ✔ select users (1.9s) 1 tests passed
passしました。
なお、このテストはWorkers側にd1のbindingがなくても動きます。
Summery
今回はMiniflareを使ってWorkersのテストを書いてみました。
軽量であり、wranglerをローカル起動しなくてもテストが動かせますし、
bindingも簡単になのでCIとかで使えそうです。