Super なんとか

先日 https://deno.land/x を見ていたら、SuperOak というモジュールを見つけた。

名前だけ見た時は、「誰かが oak を魔改造してそういう名前でも付けたか?」と中身を見ると、oak の HTTP アサーションをするモジュールだった。

スーパーバイザー的な意味での super~~であったらしい?

(2022/07/18 19:45追記: この名前の経緯について、Twitterで教えてもらった。 visionmedia/superagentの登場以降、HTTPテストライブラリには super~~ と付けるのが、慣習になっているんだそう。)

というわけで、見つけた super~~ なモジュールを試してみたい。

参考

superdeno

話の発端は SuperOak であったのだが、これは SuperDeno に依存している。
というところで、SuperDeno から試していく。

サンプルを見ると、opineを使ったことは無いので std/http で確認していく。

一番最初に書くような std/http のサーバーは、サンプル通り次のようになります。

app.ts
1
2
3
import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts";

serve(() => new Response("Hello World\n"), { port: 8080 });

これを SuperDeno で HTTP アサーションをするには、サーバーを起動するのではなく、Server クラスのインスタンスが欲しいので、次のようにできる。

app.ts
1
2
3
4
5
6
import { Server } from "https://deno.land/std@0.148.0/http/server.ts";

export const server = new Server({
handler: () => new Response("Hello World"),
port: 8080,
});
app.ts
1
2
3
import { server } from "./server.ts";

server.listenAndServe();
app_test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
import { server } from "./server.ts";
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
import { assertEquals } from "https://deno.land/std@0.148.0/testing/asserts.ts";

Deno.test("HTTP assert", async () => {
const r = await superdeno(server)
.get("/")
.expect("Content-Type", /text/)
.expect("Content-Length", "11")
.expect(200);

assertEquals(r.text, "Hello World");
});

実行すると次のようになります。

1
2
3
4
5
6
7
8
9
# アプリケーション実行
$ deno run --allow-net=0.0.0.0:8080 app.ts

# テスト実行
$ deno test --allow-net app_test.ts
running 1 test from ./app_test.ts
HTTP assert ... ok (40ms)

ok | 1 passed | 0 failed (68ms)

どうやら、テストはできているらしいが本当かわからないので、ちょっと書き換えて実行してみる。

app_test.ts(ワザと失敗するテスト)
1
2
3
4
5
6
7
8
9
10
11
12
13
import { server } from "./server.ts";
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
import { assertEquals } from "https://deno.land/std@0.148.0/testing/asserts.ts";

Deno.test("HTTP assert", async () => {
const r = await superdeno(server)
.get("/")
.expect("Content-Type", /text/)
.expect("Content-Length", "11")
.expect(201); // <= コードを変更

assertEquals(r.text, "Hello World");
});

改めて実行すると次のように。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ deno test --allow-net app_test.ts
running 1 test from ./app_test.ts
HTTP assert ... FAILED (41ms)

ERRORS

HTTP assert => ./app_test.ts:5:6
error: Error: expected 201 "Created", got 200 "OK"
return new Error(`expected ${status} "${a}", got ${res.status} "${b}"`);
^
at Test.#assertStatus (https://deno.land/x/superdeno@4.8.0/src/test.ts:624:14)
at Test.#assertFunction (https://deno.land/x/superdeno@4.8.0/src/test.ts:641:13)
at Test.#assert (https://deno.land/x/superdeno@4.8.0/src/test.ts:504:35)
at https://deno.land/x/superdeno@4.8.0/src/test.ts:479:23
at async close (https://deno.land/x/superdeno@4.8.0/src/close.ts:47:46)

FAILURES

HTTP assert => ./app_test.ts:5:6

FAILED | 0 passed | 1 failed (69ms)

error: Test failed

どうやらちゃんと動いていそうです。

superdeno もう少し掘る

サーバー側で、ヘッダーへの情報の付与や、ペイロードの付与を確認してみます。

ヘッダーの付与

サーバー側。

server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
import { Server } from "https://deno.land/std@0.148.0/http/server.ts";

export function getServer() {
return new Server({
handler: (request) => {
if (request.headers.get("accept") !== "application/json") {
return new Response("", { status: 400 });
}
return Response.json({ text: "Hello World" });
},
port: 8080,
});
}

テスト側。

app_test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { getServer } from "./server.ts";
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
import { assertEquals } from "https://deno.land/std@0.148.0/testing/asserts.ts";

Deno.test("HTTP assert", async (t) => {
await t.step("#1", async () => {
const r = await superdeno(getServer())
.get("/")
.set("Accept", "application/json")
.expect("Content-Type", /json/)
.expect(200);

const body = r.body;

assertEquals(body.text, "Hello World");
});

await t.step("#2", async () => {
await superdeno(getServer())
.get("/")
// .set("Accept", "application/json") <= ヘッダーを欠落させる
.expect(400);
});
});

このようにすることで、テストができる。
先の実装と比較して、new Server がテストのステップの都度行われるようにした。

なぜかというと、SuperDeno の引数に与えたサーバーが、1 回の実行によりクローズしてしまったため。
この対策を取らないと 2 つめにテストが行われたものは、確実に以下のエラーになっていた。

1
2
3
4
5
6
7
8
9
10
11
SuperDeno experienced an unexpected server error. Http: Server closed
at Server.listenAndServe (https://deno.land/std@0.148.0/http/server.ts:178:13)
at new Test (https://deno.land/x/superdeno@4.8.0/src/test.ts:227:43)
at Object.obj.<computed> [as get] (https://deno.land/x/superdeno@4.8.0/src/superdeno.ts:101:14)
at file:///usr/src/app/app_test.ts:30:8
at testStepSanitizer (deno:runtime/js/40_testing.js:449:13)
at asyncOpSanitizer (deno:runtime/js/40_testing.js:147:15)
at resourceSanitizer (deno:runtime/js/40_testing.js:375:13)
at exitSanitizer (deno:runtime/js/40_testing.js:432:15)
at TestContext.step (deno:runtime/js/40_testing.js:1415:19)
at file:///usr/src/app/app_test.ts:28:11

ペイロードの付与(json)

ここまで何もリクエストに付与しないで来たので、リクエストに json をつけてリクエストを試みます。

この辺りは、SuperOak にサンプルが有るくらいだったりしていて、SuperDeno には、説明が有りません。

server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Server } from "https://deno.land/std@0.148.0/http/server.ts";

export function getServer() {
return new Server({
handler: async (request) => {
if (request.headers.get("accept") !== "application/json") {
return new Response("", { status: 400 });
}
console.log(request.method);
if (request.method !== "POST") {
return new Response("", { status: 400 });
}

const json = await request.json();

return Response.json({ text: json.text });
},
port: 8080,
});
}
app_test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { getServer } from "./server.ts";
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
import { assertEquals } from "https://deno.land/std@0.148.0/testing/asserts.ts";

Deno.test("HTTP assert", async () => {
const r = await superdeno(getServer())
.post("/")
.set("Accept", "application/json")
.send({ text: "Hello Deno!" }) // <= json を送る
.expect("Content-Type", /json/)
.expect(200);

const body = r.body;

assertEquals(body.text, "Hello Deno!");
});

ペイロードの付与(Form)

json が送れたで Form も送ってみます。

server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Server } from "https://deno.land/std@0.148.0/http/server.ts";

export function getServer() {
return new Server({
handler: async (request) => {
console.log(request.method);
if (request.method !== "POST") {
return new Response("", { status: 400 });
}

const form = await request.formData();

if (form.get("text") !== "Deno!") {
return new Response("", { status: 400 });
}

return new Response("", { status: 200 });
},
port: 8080,
});
}
app_test.ts
1
2
3
4
5
6
7
8
9
10
11
import { getServer } from "./server.ts";
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
import { assertEquals } from "https://deno.land/std@0.148.0/testing/asserts.ts";

Deno.test("HTTP assert", async () => {
await superdeno(getServer())
.post("/")
.set("Content-Type", "application/x-www-form-urlencoded")
.send("text=Deno!")
.expect(200);
});

フォームの送信もテストできました。

superoak

superoak は、superdeno と同一作者のモジュールです。

これまでのものを踏まえて GET, POST(json), POST(From) を作ってみます。

server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import {
Application,
type Context,
Router,
} from "https://deno.land/x/oak@v10.4.0/mod.ts";

const router = new Router();
router.get("/", ({ response }: Context) => {
response.body = "Hello world";
});

router.post("/json", async ({ request, response }: Context) => {
if (request.headers.get("accept") !== "application/json") {
return new Response("", { status: 400 });
}
const json = await request.body().value;

if (!json.text) {
response.status = 400;
return;
}

response.body = { text: json.text };
});

router.post("/form", async ({ request, response }: Context) => {
if (request.method !== "POST") {
return new Response("", { status: 400 });
}
const form = await request.body({ type: "form" }).value;

if (form.get("text") !== "Deno!") {
return new Response("", { status: 400 });
}

response.status = 200;
});

router.get("/form", async ({ request, response }: Context) => {
response.body = `
<html>
<body>
<form METHOD="POST">
<input name="text">
<button type="submit">submit</button>
</form>
</body>
</html>
`;
});

const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());

export { app };

というところで  super~~ なモジュールを調べてみました。

oak のテストを書くとき、リクエストの内容と応答のテストをしたいと思っていたので、ドンピシャの内容でした。
ページの中からトークンを取得して、それを含めてリクエスト。なんてのを試したいので、もう少し深堀をしていくと思います。
(そこまでやるなら別のツールを使うのが筋とも感じるところもありますが)
(更新(2022/07/20) SuparOak でフォームと Cookie を送ってみる)

他に、deno-libs/superfetch というものも見つけて試してみましたが、こちらはサンプルの通り node 互換のもののようなので、見送りをしました。

ではでは。