fresh-testing-library
概要
Freshアプリケーションのユニットテスト/インテグレーションテスト用のユーティリティです。 機能
Islandコンポーネント, Handler, Middlewareなどのテストをサポート
Partialsなどの機能のエミュレーションをサポート
インストール
$MODULE_VERSIONに指定するURLはから調べられます code:json
// ...
"imports": {
"🏝️/": "./islands/",
// ...
// 以下を追加します ($MODULE_VERSIONは使用したいバージョンで置き換えます)
},
// ...
使い方
Islandコンポーネントのテスト
code:test/islands/Counter.test.tsx
import {
cleanup,
render,
setup,
userEvent,
} from "$fresh-testing-library/components.ts";
import { expect } from "$fresh-testing-library/expect.ts";
import { signal } from "@preact/signals";
import { afterEach, beforeAll, describe, it } from "$std/testing/bdd.ts";
import Counter from "🏝️/Counter.tsx";
import { default as manifest } from "./demo/fresh.gen.ts";
describe("islands/Counter.tsx", () => {
// partialsのエミュレーションを有効化したい場合は、setup()でmanifestオプションの指定が必要です (不要な場合は指定しなくても問題ありません)
beforeAll(() => setup({ manifest }));
afterEach(cleanup);
it("should work", async () => {
const count = signal(9);
const user = userEvent.setup();
const screen = render(<Counter count={count} />);
const plusOne = screen.getByRole("button", { name: "+1" });
const minusOne = screen.getByRole("button", { name: "-1" });
expect(screen.getByText("9")).toBeInTheDocument();
await user.click(plusOne);
expect(screen.queryByText("9")).not.toBeInTheDocument();
expect(screen.getByText("10")).toBeInTheDocument();
await user.click(minusOne);
expect(screen.getByText("9")).toBeInTheDocument();
expect(screen.queryByText("10")).not.toBeInTheDocument();
});
});
Middlewareのテスト
code:middleware.test.ts
import { createFreshContext } from "$fresh-testing-library/server.ts";
import { assert } from "$std/assert/assert.ts";
import { assertEquals } from "$std/assert/assert_equals.ts";
import { describe, it } from "$std/testing/bdd.ts";
import { createLoggerMiddleware } from "./demo/routes/(_middlewares)/logger.ts";
import manifest from "./demo/fresh.gen.ts";
describe("createLoggerMiddleware", () => {
it("returns a middleware which logs the information about each request", async () => {
const messages: Array<string> = [];
const testingLogger = {
info(...args: Array<unknown>) {
messages.push(args.map(String).join(""));
},
};
const middleware = createLoggerMiddleware(testingLogger);
const path = /api/users/123;
const req = new Request(http://localhost:3000${path});
const ctx = createFreshContext(req, { manifest });
await middleware(req, ctx);
assertEquals(messages, [
<-- GET ${path},
--> GET ${path} 200,
]);
});
});
Handlerのテスト
code:test/routes/api/users/id.test.ts import { createFreshContext } from "$fresh-testing-library/server.ts";
import { assert } from "$std/assert/assert.ts";
import { assertEquals } from "$std/assert/assert_equals.ts";
import { describe, it } from "$std/testing/bdd.ts";
import { handler } from "./demo/routes/api/users/id.ts"; import manifest from "./demo/fresh.gen.ts";
describe("handler.GET", () => {
it("should work", async () => {
assert(handler.GET);
const ctx = createFreshContext(req, { manifest });
assertEquals(ctx.params, { id: "1" });
const res = await handler.GET(req, ctx);
assertEquals(res.status, 200);
assertEquals(await res.text(), "bob");
});
});
Async Route Componentのテスト
createRouteContext()を使うとfreshのRouteContextオブジェクトを作成できます。stateオプションを指定すると、RouteContext.stateに対して依存注入ができます。 code:tests/routes/users/id.test.tsx import {
cleanup,
getByText,
render,
setup,
} from "$fresh-testing-library/components.ts";
import { createFreshContext } from "$fresh-testing-library/server.ts";
import { assertExists } from "$std/assert/assert_exists.ts";
import { afterEach, beforeAll, describe, it } from "$std/testing/bdd.ts";
import { default as UserDetail } from "./demo/routes/users/id.tsx"; import { default as manifest } from "./demo/fresh.gen.ts";
import { createInMemoryUsers } from "./demo/services/users.ts";
describe("routes/users/id.tsx", () => { beforeAll(() => setup({ manifest }));
afterEach(cleanup);
it("should work", async () => {
const state = { users: createInMemoryUsers() };
const ctx = createFreshContext<void, typeof state>(req, {
// NOTE: If manifest option was specified in setup(), it can be omitted here.
// It is also possible to inject dependencies into ctx.state with state option.
state,
});
const screen = render(await UserDetail(req, ctx));
const group = screen.getByRole("group");
assertExists(getByText(group, "bar"));
});
});
サブモジュール
code:typescript
import { createFreshContext } from "$fresh-testing-library/server.ts";
import {
cleanup,
render,
setup,
userEvent,
} from "$fresh-testing-library/components.ts";
import { expect } from "$fresh-testing-library/expect.ts";
Tips
Denoの--locationオプションとmswを併用すると、相対URLを指定してfetchを読んでいるコードをテストできます。 code:deno.json
{
"imports": {
// Add the following lines:
"msw": "npm:msw@2.0.8",
"msw/node": "npm:msw@2.0.8/node"
}
}
以下のようなイメージで使用できます。
code:msw.test.ts
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { expect } from "$fresh-testing-library/expect.ts";
import { afterAll, beforeAll, describe, it } from "$std/testing/bdd.ts";
// --locationオプションを指定しておくと、locationが定義されます。
expect(location).toBeTruthy();
describe("msw", () => {
const server = setupServer(
http.get(${location.origin}/api/user, () =>
HttpResponse.json({
id: 1,
name: "foo",
})),
);
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterAll(() => server.close());
it("can be used to intercept requests", async () => {
const res = await fetch("/api/user");
expect(await res.json()).toEqual({ id: 1, name: "foo" });
});
});
補足
用途に応じて使い分けることを想定しています。
Islandコンポーネントをその他の要素とは独立してテストする
ctx.stateへの依存注入などにより、Handler/Middlewareなどをその他の要素とは独立してテストする
TODO/やりたいこと
Fresh組み込みコンポーネントや機能のサポートの拡充
App/Layoutのサポート
vitest-preview/jest-preview相当の仕組み
これは別のライブラリとして分離した方がよいのかもしれない
リンク