[Rust] Miniflareでテストを実行 [Cloudflare Workers]

2023.10.26

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

References