Deno Advent Calendar 2023 13 日目の記事です。
Fresh のプラグインをいくつか作って公開しているが、公開するからにはちゃんとテストも書きたかった。 そして昨今Freshの動きがなかなか激しく、新しいバージョンでも動作するための定期実行するテストも用意したかった。
数件実装して、方法が固まったので書き残したい。同じような需要の人に答えられたらうれしい。
参考
テストの実装方針 github actions 上でのテストを用意するにあたり、以下を要件にする。
Fresh の最新版に対してテストする
Fresh 本体への直接的な書き換えを伴う拡張をせずにテストする
テストのカバレッジの取得、表示をする
この要件を達成するために、以下の事を行う。
プラグインAを導入することで、動作に干渉/拡張するレスポンスを返す「routes(及びハンドラ)」を設定するテスト用のプラグインBを作る。 以下順を追って説明する。
ディレクトリ構成 ディレクトリ構成は、あまり特別なことはしていない認識。 基本的な構造としては次のようになる想定。
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 | .gitignore | deno.json | deps.ts | LICENSE | mod.ts | README.md +---.github | \---workflows | test.yml +---src | | consts.ts | | type.ts | +---handlers | | csrf_handler.ts | +---plugins | | csrf_plugin.ts | \---utils | request.ts | response.ts \---tests | csrf_test.ts | test_deps.ts +---config | csrf_fresh.config.ts +---plugins | test_plugin.ts \---routes test_route.tsx
先に上げた プラグインAの機能を使うroutesを拡張するプラグインBが、testディレクトリ以下のpluginなどになる。
github actions の job定義 github actions テストは概ね以下のように定義している。
.github/workflows/test.yml 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 name: Test on: push: branches: ["main" , "test" ] pull_request: branches: ["main" , "test" ] schedule: - cron: '0 0 * * 1' jobs: test: permissions: contents: read runs-on: ubuntu-latest steps: - name: Setup repo uses: actions/checkout@v3 - name: Setup Deno uses: denoland/setup-deno@v1 - name: Check version run: deno -V - name: Verify formatting run: deno fmt --check - name: Install Fresh run: deno run -A https://deno.land/x/fresh/init.ts ./tests/work --force --twind --tailwind --vscode - name: Move deno.json run: mv ./tests/work/deno.json ./deno.json - name: View deno.json run: cat ./deno.json - name: Run tests run: deno test --unstable --allow-read --allow-write --allow-env --allow-net --no-check --coverage=./coverage - name: View coverage run: | # reference: https://github.com/jhechtf/code-coverage deno install --allow-read --no-check -n code-coverage https://deno.land/x/code_coverage/cli.ts && deno coverage --exclude=tests/work/ --lcov --output=lcov.info ./coverage/ && code-coverage --file lcov.info
Fresh を自動でtests以下のディレクトリにインストールする ポイントになるのは、--twind
などのオプションで各種質問をスキップすること。tests/work
をインストール対象にしているが、ぶつからなければtests以下のどこでもよい。 Freshのインストール時の質問が増えた時には、このテストは壊れる予定。
deno.json を移動する deno.json は階層を上にたどってくれるが、より深いほうに探索してくれない(と認識している)ので、移動する。 これをしておくと、上の階層で $fresh/server.ts
を参照していたとしても、移動したdeno.jsonに記載のimportsが参照される。 このことで最新のFreshが参照されることになる。
deno.json の中身を表示 仮にテストが失敗した時にどのバージョンで失敗したのか知るには出力させるのが楽。
テスト実行 Deno KV がかかわるもののテストをしているので --unstable
が入っているが、基本不要。 カバレッジを出力しておく。
jhechtf/code-coverage をインストールし、コードカバレッジを出力。 これも、github actions のログで確認できる。
プラグインAの機能を使うテスト用プラグインB fresh_csrf を例に紹介します。
fresh_csrfプラグインの機能は、CSRF対策用のトークンの発行/検証をする。 なので、この発行されたトークンを使用する機能を使うプラグインを用意する。
fresh.config.ts の代わり Fresh をインストールすると、fresh.config.ts
が作成されている。このファイルは、dev.ts main.ts から参照され、各種プラグインの設定などができる。
このファイルを別の内容で呼び出せば、Freshのインストール直後の状態のまま、別のプラグインが適用できる。
tests/config/csrf_fresh.config.ts 1 2 3 4 5 6 7 8 9 10 11 import { defineConfig } from "$fresh/server.ts" ;import { getCsrfPlugin } from "../../mod.ts" ; import { testPlugin } from "../plugins/test_plugin.ts" ; export default defineConfig ({ plugins : [ await getCsrfPlugin (await Deno .openKv (":memory:" )), testPlugin, ], });
今回の場合はroutesの設定なので、あまり気にせずプラグインを登録できる。 もし拡張した機能をミドルウェアで使う場合、テスト用ミドルウェアは機能提供するミドルウェアの後に記述する。 書いた順番がミドルウェアの呼び出し順序と一致するはず。
テスト用プラグイン テスト用のプラグインは以下のようにしている。
tests/plugins/test_plugin.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { PageProps , Plugin } from "$fresh/server.ts" ;import TestComponent , { handler } from "../routes/test_route.tsx" ;import { ComponentType } from "preact" ;export const testPlugin : Plugin = { name : "TestPlugin" , routes : [ { handler, component : TestComponent as ComponentType <PageProps >, path : "/csrf" , }, { handler, component : TestComponent as ComponentType <PageProps >, path : "/sub/csrf" , }, ], };
ここについては、特にひねったところはない。テスト用のコンポーネントと、ハンドラを設定しているだけ。 同じコンポーネントを複数のパスに設定もできる。 プラグインを使うとFreshのディレクトリベースルーティングから外れることができるのでこういう楽もできる。
テスト用 routes テスト用プラグイン で読み込んでいたroutesが次のようになる。
tests/routes/test_route.tsx 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 import { FreshContext , Handlers , PageProps } from "$fresh/server.ts" ;import type { WithCsrf } from "../../mod.ts" ;export const handler : Handlers <unknown , WithCsrf > = { async GET (_req: Request, ctx: FreshContext ) { const res = await ctx.render (); return res; }, async POST ( req: Request, ctx: FreshContext<WithCsrf>, ) { const form = await req.formData (); const token = form.get ("csrf" ); const text = form.get ("text" ); if (!ctx.state .csrf .csrfVerifyFunction (token?.toString () ?? null )) { const res = new Response (null , { status : 302 , headers : { Location : "/csrf" , }, }); return res; } ctx.state .csrf .updateKeyPair (); const res = await ctx.render ({ text }); return res; }, }; export default function Test ( props: PageProps<{ text: string }, WithCsrf>, ) { return ( <div > <p > {props?.data?.text || "NO SET"}</p > <form method ="post" > <input type ="hidden" name ="csrf" value ={props.state.csrf.getTokenStr()} /> <input type ="text" name ="text" /> <button class ="button border" > Submit</button > </form > </div > ); }
先の通り、プラグインの機能を使うroutesにする必要がある。 handlerでトークンの検証。コンポーネントでトークンの設定をしている。
テストコード テストコードは以下のようになる。
tests/csrf_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 25 26 27 28 29 30 31 32 import { createHandler, ServeHandlerInfo } from "$fresh/server.ts" ;import manifest from "./work/fresh.gen.ts" ;import config from "./config/csrf_fresh.config.ts" ;import { expect, FakeTime } from "./test_deps.ts" ;const CONN_INFO : ServeHandlerInfo = { remoteAddr : { hostname : "127.0.0.1" , port : 53496 , transport : "tcp" }, }; Deno .test ("Csrf Test" , async (t) => { await t.step ("Get Tokens" , async () => { const handler = await createHandler (manifest, config); const res = await handler (new Request ("http://127.0.0.1/csrf" ), CONN_INFO ); expect (res.status ).toBe (200 ); const text = await res.text (); expect (text.includes ("<p>NO SET</p>" )).toBeTruthy (); const csrfCookieToken = res.headers .get ("set-cookie" )! .split ("csrf_token=" )[1 ] .split (";" )[0 ]; const csrfToken = text .split ('<input type="hidden" name="csrf" value="' )[1 ] .split ('"/' )[0 ]; expect (csrfCookieToken).not .toMatch (/^$/ ); expect (csrfToken).not .toMatch (/^$/ ); }); });
テスト用プラグインで用意したconfigを設定する
テスト用のプラグインで設定したroutesにアクセス
レスポンスに、機能提供するプラグインが設定したCookieの設定検証する
レスポンスボディ本体の設定箇所にトークンが埋まっていることを検証する。
このテストでは、機能提供するプラグインの機能を使ったテスト用プラグインのレスポンスを検証している。 このことで、最新のFreshを使いつつプラグインの機能を使ったレスポンスの検証ができる。
実行結果 github actions のログには次のように出力される。
actions ログ(抜粋) 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 Verification Tokens Failed(Illegal Cookie Token) ... ok (8ms) Verification Tokens Failed(Illegal Token) ... ok (5ms) Verification Tokens Failed(Not set Cookie token) ... ok (4ms) Verification Tokens Failed(Not set Token) ... ok (6ms) Verification Tokens Failed(Token Time Out) ... ok (5ms) Csrf Test ... ok (67ms) ok | 9 passed (8 steps) | 0 failed (194ms) Run # reference: https://github.com/jhechtf/code-coverage Download https://deno.land/x/code_coverage/cli.ts Warning Implicitly using latest version (0.3.1) for https://deno.land/x/code_coverage/cli.ts Download https://deno.land/x/code_coverage@0.3.1/cli.ts Download https://deno.land/x/code_coverage@0.3.1/args.ts Download https://deno.land/x/code_coverage@0.3.1/deps.ts Download https://deno.land/x/code_coverage@0.3.1/mod.ts Download https://deno.land/std@0.208.0/streams/text_line_stream.ts Download https://deno.land/x/ascii_table@v0.1.0/mod.ts Download https://deno.land/x/code_coverage@0.3.1/projectCoverage.ts Download https://deno.land/x/code_coverage@0.3.1/fileCoverage.ts Download https://deno.land/x/code_coverage@0.3.1/range.ts ✅ Successfully installed code-coverage /home/runner/.deno/bin/code-coverage .-----------------------------------------------------------------------. | File Path | Coverage | Lines Without Coverage | |-----------------------------------|----------|------------------------| | deps.ts | 100.00% | n/a | | mod.ts | 100.00% | n/a | | src/consts.ts | 100.00% | n/a | | src/handlers/csrf_handler.ts | 96.67% | 122,136-138 | | src/plugins/csrf_plugin.ts | 100.00% | n/a | | tests/config/csrf_fresh.config.ts | 100.00% | n/a | | tests/plugins/test_plugin.ts | 100.00% | n/a | | tests/routes/test_route.tsx | 100.00% | n/a | | tests/test_deps.ts | 100.00% | n/a | | Totals: | 98.28% | | '-----------------------------------------------------------------------'
(有意なテストが書けているのかという話は別にあれ、カバレッジとしては悪い数字ではないはず。)
github actions 上で Fresh のプラグインを最新のFreshに適用してテストしてみました。
いくつかのリポジトリで稼働させてパターンが固まったので今後もこの方法で対応して行く予定。
まだ、ミドルウェアの内部的な呼び出しをspyなりするのは実績が無い。 これもテスト用に用意するミドルウェアを何かしら加工して対応できる見込み。 ただし、今回のケースよりは煩雑なものになってくるはず。
では。