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とかで使えそうです。