Oakでルート用のミドルウェアをテストしたい話
oakでルート用のミドルウェアに対するテストを行おうとした時に躓いたので、そのための忘備録。
標準のモッキングライブラリ
Oakではモック用のユーティリティ関数が用意されている。
https://deno.land/x/oak@v7.7.0/docs/testing.md
通常のミドルウェアに対するユニットテストでは、testing
名前空間で用意されている関数で問題ない。
しかし、ルート用のミドルウェアをテストしたい時、内部でリクエストボディを参照したい場合にtesting.createMockContext
では、request.body
を設定することができない。
なので意図したテストをすることが難しい。
Object literal may only specify known properties, and 'request' does not exist in type 'MockContextOptions<string, ParamsDictionary, Record<string, any>>'
Superoakを使う
SuperoakはOakフレームワークのHTTPテストを行うために必要な設定等の抽象度を高めて、よりテストしやすくするために作られたモジュール。
まず、テストしたいルート用のミドルウェアを定義する。
ミドルウェアは、router.post("/user", (ctx) => {
ctx.response.body = "Post!";
});
の様に、直接コールバック関数として定義しないで、個別に定義をしておく。
// routes/auth.ts import { Middleware } from "https://deno.land/x/oak@v10.5.1/mod.ts"; export const signup: Middleware = async (ctx) => {}
Deno.testに登録したfn内で、ルートの設定とアプリの設定を行う。
DenoDBを使用している場合は、リクエストを実行する前にawait db.sync();
を呼び出しておく。
import { Application, Router } from "https://deno.land/x/oak@v10.5.1/mod.ts"; Deno.test({ name: "smoke test", async fn() { .... const router = new Router(); router.post("/auth/signup", signup); const app = new Application(); app.use(router.routes()); app.use(router.allowedMethods()); await db.sync(); ... } });
今回はform-data
でデータを送信することを想定する。
Superoakのサンプルコードでは、.set("Content-Type", "application/json")
を使用してContent Typeを指定している。
しかし、Form Dataを送信したい時に、.set("Content-Type", "multipart/form-data")
を指定してしまうと、 "form-data" does not contain a valid boundary.
というエラーが発生してしまう。
なので、ここはあえてContent-Typeは設定しないでおく。
javascript - fetch - Missing boundary in multipart/form-data POST - Stack Overflow
formData.append("email", "メールアドレス")
のように値をリクエストボディに設定したい場合は、field
メソッドを使用する。
import { superoak } from "https://deno.land/x/superoak@4.7.0/mod.ts"; ... const request = await superoak(app); await request .post("/auth/signup") .field("email", email) .field("password", password) .expect(200) .expect((response) => { assertExists(response.body.token); assertExists(response.body.id); assertEquals(response.body.email, email); });
最終的には次の様な感じ
// auth.signup.test.ts import faker from "https://cdn.skypack.dev/@faker-js/faker@v5.5.3?dts"; import { superoak } from "https://deno.land/x/superoak@4.7.0/mod.ts"; import { Application, Router } from "https://deno.land/x/oak@v10.5.1/mod.ts"; import { assertExists, assertEquals, } from "https://deno.land/std@0.144.0/testing/asserts.ts"; import { signup } from "/routes/auth.ts"; import db from '/db.ts' Deno.test({ name: "smoke test", async fn() { // emailを準備する const email = faker.internet.email(); // passwordを準備する const password = faker.internet.password(); // ルートを設定する const router = new Router(); router.post("/auth/signup", signup); // Oakアプリを準備する const app = new Application(); app.use(router.routes()); app.use(router.allowedMethods()); // DBを同期させる await db.sync({ drop: true }); // リクエストを準備する const request = await superoak(app); await request .post("/auth/signup") .field("email", email) .field("password", password) .expect(200) .expect((response) => { assertExists(response.body.token); assertExists(response.body.id); assertEquals(response.body.email, email); }); }, });
SuperOakのインスタンスは1つのリクエストにつき1つ用意しないといけないことに注意する。
https://github.com/cmorten/superoak#request-has-been-terminated-error