diff --git a/AGENTS.md b/AGENTS.md index b366a81..d5c9ce5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,10 @@ - To run tests for a specific file, use `deno task test ./src/server.test.tsx` - Prefer running tests more narrowly to reduce token usage. Output is large when running all tests. + - Use `--reporter=dot` when running all tests, or any run where you don't need + a test's console output — it prints detail only for failures, keeping output + small. Drop it only when you need debug logs from the specific test you're + inspecting. - Temporarily change `describe` or `it` to `describe.only` or `it.only` to focus specific test groups or cases. - Workspace-specific test tasks are available: diff --git a/docs/getting-started.md b/docs/getting-started.md index 23d7de8..c992d1d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -228,13 +228,11 @@ APP_ENV=production NODE_ENV=production ``` -**`.env.test`** - Test settings (uses a different port so tests can run -alongside the dev server): +**`.env.test`** - Test settings: ```bash APP_ENV=test OTEL_SERVICE_NAME=my-app-test -DENO_SERVE_ADDRESS=tcp:0.0.0.0:8100 ``` > **Note:** These environment files contain only non-sensitive configuration. @@ -334,13 +332,17 @@ deno task serve:prod ### Running Tests -Run your test suite on a separate port (8100) so tests don't conflict with the -dev server: +Run your test suite: ```bash deno task test ``` +Tests that spawn the server inject `DENO_SERVE_ADDRESS=tcp:127.0.0.1:0` so it +binds an OS-assigned free port, then discover the actual URL from the server's +`Listening on` line. Parallel test runs never collide with each other or with +the dev server. + ### Code Quality Check formatting, linting, and type errors: diff --git a/docs/tutorials/blog.md b/docs/tutorials/blog.md index 9124f76..3183ab5 100644 --- a/docs/tutorials/blog.md +++ b/docs/tutorials/blog.md @@ -915,7 +915,7 @@ Create `main.test.ts` to test the server serves your application: ```typescript // main.test.ts -import { assertEquals, assertStringIncludes } from "@std/assert"; +import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { resolve } from "@std/path/resolve"; import { mergeReadableStreams, TextLineStream } from "@std/streams"; import { describe, it } from "@std/testing/bdd"; @@ -948,6 +948,7 @@ describe("serves application when running main.ts", () => { `--env-file=${resolve(import.meta.dirname!, "./.env.test")}`, resolve(import.meta.dirname!, "./main.ts"), ], + env: { DENO_SERVE_ADDRESS: "tcp:127.0.0.1:0" }, stdout: "piped", stderr: "piped", }); @@ -956,13 +957,16 @@ describe("serves application when running main.ts", () => { .pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); + let url = ""; for await (const line of stdout.values({ preventCancel: true })) { - if (line.includes("Listening on")) { + if (line.includes("Listening on ")) { + url = line.split("Listening on ")[1].split(" ")[0]; break; } } + assert(url, "server did not report its address"); - const res = await fetch("http://localhost:8100/"); + const res = await fetch(url); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "text/html; charset=utf-8"); const html = await res.text(); @@ -975,7 +979,10 @@ describe("serves application when running main.ts", () => { This test: - Verifies static files are served correctly -- Spawns the actual server process and makes HTTP requests +- Spawns the actual server process with `DENO_SERVE_ADDRESS=tcp:127.0.0.1:0` so + the OS assigns a free port — parallel test runs never collide +- Discovers the server's actual URL from its `Listening on` line before making + HTTP requests - Checks the HTML response contains expected content For more testing patterns including loader testing, action testing, mocking, and diff --git a/example/.env.test b/example/.env.test index 0ae088e..13db43c 100644 --- a/example/.env.test +++ b/example/.env.test @@ -1,4 +1,3 @@ APP_ENV=test OTEL_SERVICE_NAME=test -DENO_SERVE_ADDRESS=tcp:0.0.0.0:8100 DATABASE_URL=postgresql://test:password@localhost:9401/test diff --git a/example/README.md b/example/README.md index b423849..a8f0ea2 100644 --- a/example/README.md +++ b/example/README.md @@ -70,7 +70,8 @@ Run your test suite: deno task test ``` -Tests run on port 8100 by default so they don't conflict with the dev server. +Tests use an OS-assigned port so they don't conflict with the dev server or with +parallel test runs. ## Code Quality diff --git a/example/main.test.ts b/example/main.test.ts index 890e468..f9e9e77 100644 --- a/example/main.test.ts +++ b/example/main.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertStringIncludes } from "@std/assert"; +import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { resolve } from "@std/path/resolve"; import { mergeReadableStreams, TextLineStream } from "@std/streams"; import { describe, it } from "@std/testing/bdd"; @@ -31,6 +31,7 @@ describe("serves application when running main.ts", () => { `--env-file=${resolve(import.meta.dirname!, "./.env.test")}`, resolve(import.meta.dirname!, "./main.ts"), ], + env: { DENO_SERVE_ADDRESS: "tcp:127.0.0.1:0" }, stdout: "piped", stderr: "piped", }); @@ -39,20 +40,16 @@ describe("serves application when running main.ts", () => { .pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); + let url = ""; for await (const line of stdout.values({ preventCancel: true })) { - if (line.includes("Listening on")) { - const address = Deno.build.os === "windows" - ? "http://localhost:8100/" - : "http://0.0.0.0:8100/ (http://localhost:8100/)"; - assertEquals( - line, - `Listening on ${address}`, - ); + if (line.includes("Listening on ")) { + url = line.split("Listening on ")[1].split(" ")[0]; break; } } + assert(url, "server did not report its address"); - const res = await fetch("http://localhost:8100/"); + const res = await fetch(url); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "text/html; charset=utf-8"); const html = await res.text(); diff --git a/src/utils/global-jsdom.ts b/src/utils/global-jsdom.ts index c22aab8..9715773 100644 --- a/src/utils/global-jsdom.ts +++ b/src/utils/global-jsdom.ts @@ -18,7 +18,7 @@ import globalJsdom from "global-jsdom"; import { stubFormData } from "./testing.ts"; -// DENO_SERVE_ADDRESS format: "tcp:0.0.0.0:8100". +// DENO_SERVE_ADDRESS format: "tcp:0.0.0.0:8000". const address = Deno.env.get("DENO_SERVE_ADDRESS"); let port = 8000; if (address) { diff --git a/templates/minimal/.env.test b/templates/minimal/.env.test index 62dff5a..94e61b3 100644 --- a/templates/minimal/.env.test +++ b/templates/minimal/.env.test @@ -1,3 +1,2 @@ APP_ENV=test OTEL_SERVICE_NAME=test -DENO_SERVE_ADDRESS=tcp:0.0.0.0:8100 diff --git a/templates/minimal/README.md b/templates/minimal/README.md index 17fab32..8399307 100644 --- a/templates/minimal/README.md +++ b/templates/minimal/README.md @@ -67,7 +67,8 @@ Run your test suite: deno task test ``` -Tests run on port 8100 by default so they don't conflict with the dev server. +Tests use an OS-assigned port so they don't conflict with the dev server or with +parallel test runs. ## Code Quality diff --git a/templates/minimal/main.test.ts b/templates/minimal/main.test.ts index e81d094..262d1af 100644 --- a/templates/minimal/main.test.ts +++ b/templates/minimal/main.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertStringIncludes } from "@std/assert"; +import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { resolve } from "@std/path/resolve"; import { mergeReadableStreams, TextLineStream } from "@std/streams"; import { describe, it } from "@std/testing/bdd"; @@ -31,6 +31,7 @@ describe("serves application when running main.ts", () => { `--env-file=${resolve(import.meta.dirname!, "./.env.test")}`, resolve(import.meta.dirname!, "./main.ts"), ], + env: { DENO_SERVE_ADDRESS: "tcp:127.0.0.1:0" }, stdout: "piped", stderr: "piped", }); @@ -39,20 +40,16 @@ describe("serves application when running main.ts", () => { .pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); + let url = ""; for await (const line of stdout.values({ preventCancel: true })) { - if (line.includes("Listening on")) { - const address = Deno.build.os === "windows" - ? "http://localhost:8100/" - : "http://0.0.0.0:8100/ (http://localhost:8100/)"; - assertEquals( - line, - `Listening on ${address}`, - ); + if (line.includes("Listening on ")) { + url = line.split("Listening on ")[1].split(" ")[0]; break; } } + assert(url, "server did not report its address"); - const res = await fetch("http://localhost:8100/"); + const res = await fetch(url); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "text/html; charset=utf-8"); const html = await res.text(); diff --git a/templates/postgres/.env.test b/templates/postgres/.env.test index 7f83d6c..0988e5b 100644 --- a/templates/postgres/.env.test +++ b/templates/postgres/.env.test @@ -1,4 +1,3 @@ APP_ENV=test OTEL_SERVICE_NAME=test -DENO_SERVE_ADDRESS=tcp:0.0.0.0:8100 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app_test diff --git a/templates/postgres/README.md b/templates/postgres/README.md index ee8ba1b..566da13 100644 --- a/templates/postgres/README.md +++ b/templates/postgres/README.md @@ -83,9 +83,9 @@ point at your managed PostgreSQL instance. deno task test ``` -Tests run on port 8100 and connect to the `app_test` database, so make sure -PostgreSQL is running and migrated (`deno task docker:start` then -`deno task db:migrate`) before running them. +Tests use an OS-assigned port (parallel-safe) and connect to the `app_test` +database, so make sure PostgreSQL is running and migrated +(`deno task docker:start` then `deno task db:migrate`) before running them. Any test that triggers a query should call `await closeDb()` in an `afterAll` hook (see [services/message.test.ts](services/message.test.ts)) to release the diff --git a/templates/postgres/main.test.ts b/templates/postgres/main.test.ts index 992a5de..9771e25 100644 --- a/templates/postgres/main.test.ts +++ b/templates/postgres/main.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertStringIncludes } from "@std/assert"; +import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { resolve } from "@std/path/resolve"; import { mergeReadableStreams, TextLineStream } from "@std/streams"; import { describe, it } from "@std/testing/bdd"; @@ -31,6 +31,7 @@ describe("serves application when running main.ts", () => { `--env-file=${resolve(import.meta.dirname!, "./.env.test")}`, resolve(import.meta.dirname!, "./main.ts"), ], + env: { DENO_SERVE_ADDRESS: "tcp:127.0.0.1:0" }, stdout: "piped", stderr: "piped", }); @@ -39,20 +40,16 @@ describe("serves application when running main.ts", () => { .pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); + let url = ""; for await (const line of stdout.values({ preventCancel: true })) { - if (line.includes("Listening on")) { - const address = Deno.build.os === "windows" - ? "http://localhost:8100/" - : "http://0.0.0.0:8100/ (http://localhost:8100/)"; - assertEquals( - line, - `Listening on ${address}`, - ); + if (line.includes("Listening on ")) { + url = line.split("Listening on ")[1].split(" ")[0]; break; } } + assert(url, "server did not report its address"); - const res = await fetch("http://localhost:8100/"); + const res = await fetch(url); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "text/html; charset=utf-8"); const html = await res.text(); diff --git a/templates/tailwindcss/.env.test b/templates/tailwindcss/.env.test index 62dff5a..94e61b3 100644 --- a/templates/tailwindcss/.env.test +++ b/templates/tailwindcss/.env.test @@ -1,3 +1,2 @@ APP_ENV=test OTEL_SERVICE_NAME=test -DENO_SERVE_ADDRESS=tcp:0.0.0.0:8100 diff --git a/templates/tailwindcss/README.md b/templates/tailwindcss/README.md index 17fab32..8399307 100644 --- a/templates/tailwindcss/README.md +++ b/templates/tailwindcss/README.md @@ -67,7 +67,8 @@ Run your test suite: deno task test ``` -Tests run on port 8100 by default so they don't conflict with the dev server. +Tests use an OS-assigned port so they don't conflict with the dev server or with +parallel test runs. ## Code Quality diff --git a/templates/tailwindcss/main.test.ts b/templates/tailwindcss/main.test.ts index 8ea561a..e50a956 100644 --- a/templates/tailwindcss/main.test.ts +++ b/templates/tailwindcss/main.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertStringIncludes } from "@std/assert"; +import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { resolve } from "@std/path/resolve"; import { mergeReadableStreams, TextLineStream } from "@std/streams"; import { describe, it } from "@std/testing/bdd"; @@ -31,6 +31,7 @@ describe("serves application when running main.ts", () => { `--env-file=${resolve(import.meta.dirname!, "./.env.test")}`, resolve(import.meta.dirname!, "./main.ts"), ], + env: { DENO_SERVE_ADDRESS: "tcp:127.0.0.1:0" }, stdout: "piped", stderr: "piped", }); @@ -39,20 +40,16 @@ describe("serves application when running main.ts", () => { .pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); + let url = ""; for await (const line of stdout.values({ preventCancel: true })) { - if (line.includes("Listening on")) { - const address = Deno.build.os === "windows" - ? "http://localhost:8100/" - : "http://0.0.0.0:8100/ (http://localhost:8100/)"; - assertEquals( - line, - `Listening on ${address}`, - ); + if (line.includes("Listening on ")) { + url = line.split("Listening on ")[1].split(" ")[0]; break; } } + assert(url, "server did not report its address"); - const res = await fetch("http://localhost:8100/"); + const res = await fetch(url); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "text/html; charset=utf-8"); const html = await res.text(); diff --git a/templates/tanstack/.env.test b/templates/tanstack/.env.test index 62dff5a..94e61b3 100644 --- a/templates/tanstack/.env.test +++ b/templates/tanstack/.env.test @@ -1,3 +1,2 @@ APP_ENV=test OTEL_SERVICE_NAME=test -DENO_SERVE_ADDRESS=tcp:0.0.0.0:8100 diff --git a/templates/tanstack/README.md b/templates/tanstack/README.md index 17fab32..8399307 100644 --- a/templates/tanstack/README.md +++ b/templates/tanstack/README.md @@ -67,7 +67,8 @@ Run your test suite: deno task test ``` -Tests run on port 8100 by default so they don't conflict with the dev server. +Tests use an OS-assigned port so they don't conflict with the dev server or with +parallel test runs. ## Code Quality diff --git a/templates/tanstack/main.test.ts b/templates/tanstack/main.test.ts index 262c1e6..7d59b65 100644 --- a/templates/tanstack/main.test.ts +++ b/templates/tanstack/main.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertStringIncludes } from "@std/assert"; +import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { resolve } from "@std/path/resolve"; import { mergeReadableStreams, TextLineStream } from "@std/streams"; import { describe, it } from "@std/testing/bdd"; @@ -31,6 +31,7 @@ describe("serves application when running main.ts", () => { `--env-file=${resolve(import.meta.dirname!, "./.env.test")}`, resolve(import.meta.dirname!, "./main.ts"), ], + env: { DENO_SERVE_ADDRESS: "tcp:127.0.0.1:0" }, stdout: "piped", stderr: "piped", }); @@ -39,20 +40,16 @@ describe("serves application when running main.ts", () => { .pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); + let url = ""; for await (const line of stdout.values({ preventCancel: true })) { - if (line.includes("Listening on")) { - const address = Deno.build.os === "windows" - ? "http://localhost:8100/" - : "http://0.0.0.0:8100/ (http://localhost:8100/)"; - assertEquals( - line, - `Listening on ${address}`, - ); + if (line.includes("Listening on ")) { + url = line.split("Listening on ")[1].split(" ")[0]; break; } } + assert(url, "server did not report its address"); - const res = await fetch("http://localhost:8100/"); + const res = await fetch(url); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "text/html; charset=utf-8"); const html = await res.text(); diff --git a/tutorials/blog/.env.test b/tutorials/blog/.env.test index 62dff5a..94e61b3 100644 --- a/tutorials/blog/.env.test +++ b/tutorials/blog/.env.test @@ -1,3 +1,2 @@ APP_ENV=test OTEL_SERVICE_NAME=test -DENO_SERVE_ADDRESS=tcp:0.0.0.0:8100 diff --git a/tutorials/blog/README.md b/tutorials/blog/README.md index 17fab32..8399307 100644 --- a/tutorials/blog/README.md +++ b/tutorials/blog/README.md @@ -67,7 +67,8 @@ Run your test suite: deno task test ``` -Tests run on port 8100 by default so they don't conflict with the dev server. +Tests use an OS-assigned port so they don't conflict with the dev server or with +parallel test runs. ## Code Quality diff --git a/tutorials/blog/main.test.ts b/tutorials/blog/main.test.ts index 815f8fe..c62526a 100644 --- a/tutorials/blog/main.test.ts +++ b/tutorials/blog/main.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertStringIncludes } from "@std/assert"; +import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { resolve } from "@std/path/resolve"; import { mergeReadableStreams, TextLineStream } from "@std/streams"; import { describe, it } from "@std/testing/bdd"; @@ -31,6 +31,7 @@ describe("serves application when running main.ts", () => { `--env-file=${resolve(import.meta.dirname!, "./.env.test")}`, resolve(import.meta.dirname!, "./main.ts"), ], + env: { DENO_SERVE_ADDRESS: "tcp:127.0.0.1:0" }, stdout: "piped", stderr: "piped", }); @@ -39,20 +40,16 @@ describe("serves application when running main.ts", () => { .pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); + let url = ""; for await (const line of stdout.values({ preventCancel: true })) { - if (line.includes("Listening on")) { - const address = Deno.build.os === "windows" - ? "http://localhost:8100/" - : "http://0.0.0.0:8100/ (http://localhost:8100/)"; - assertEquals( - line, - `Listening on ${address}`, - ); + if (line.includes("Listening on ")) { + url = line.split("Listening on ")[1].split(" ")[0]; break; } } + assert(url, "server did not report its address"); - const res = await fetch("http://localhost:8100/"); + const res = await fetch(url); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "text/html; charset=utf-8"); const html = await res.text();