Deno Fresh Testing

Deno Fresh Testing

End-to-end Site Tests

📝 Deno Fresh Testing

In this post, we see how you can set up Deno Fresh Testing on your Fresh app. We focus on end-to-end testing, though you can use the APIs seen here for unit testing utility functions in your project, for example.

Unit testing typically shines a spotlight on a single function or component; that is, to check that it behaves as expected. Those tests are performed in isolation, and integration tests might be added to check for side effects that arising from combining components. You will combine components in a way they will be used in production for integration tests, though end-to-end tests are an ideal way to check everything works as expected. End-to-end testing is applied at a higher level, interacting with the app as the end user would. That applies both to HTML page contents and API routes. Ideally, end-to-end test design makes no assumptions on the actual implementation used.

We focus on that last kind of test here, though there is an outline of running Deno unit tests in the post on Getting Started with Deno Fresh.

🧱 What are we Building?

It makes most sense to design tests for an app you are working on, rather than building out an app just for the sake of seeing tests. For that reason, we focus on adding tests to an existing Deno project. This might be a nascent project, if you prefer to follow the test-driven development (TDD) approach.

Deno Fresh Testing: Screen capture shows output from running Deno test in the Terminal, with all tests (one test on the Jokes A P I route and all three on the hime route) passing.

We add tests to a very basic Deno Fresh app, which should help get started with adding tests to your own app. The full code is available in a GitHub repo (link further down).

Hopefully you can benefit from the post independently of which testing philosophy you prefer. Let’s get started!

⚙️ Project Config

Deno has a built-in test runner, which you can call with the deno test command. This looks for test files within your project (we use the something_test.ts naming pattern in this post, though Deno will also automatically pick up something.test.ts files). If you prefer another pattern, deno.json accepts test.include, as well as test.exclude, letting you customize the test search paths. The Deno Standard Library provides the testing assertions which we need. For DOM testing we add deno_dom here to write queries on returned HTML.

Update deno.json adding a test script, as well as the deno-dom import:

{
  "lock": false,
  "tasks": {
    "start": "deno run -A --watch=static/,routes/ dev.ts",
    "test": "deno test --allow-env --allow-read --allow-net",
    "update": "deno run -A -r https://fresh.deno.dev/update ."
  },
  "imports": {
    "@/": "./",
    "$fresh/": "https://deno.land/x/fresh@1.3.1/",
    "deno-dom/": "https://deno.land/x/deno_dom@v0.1.38/",
    "preact": "https://esm.sh/preact@10.15.1",
    "preact/": "https://esm.sh/preact@10.15.1/",
    "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0",
    "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
    "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
    "$std/": "https://deno.land/std@0.196.0/"
  },
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

HTML Lang Attribute

If your site is not written in English (as spoken in the US), you will want to include an HTML lang attribute on generated HTML pages. In Deno Fresh, this is done by providing the language in the start method options in main.ts:

import { start } from "$fresh/server.ts";
import "$std/dotenv/load.ts";
import { startOptions } from "@/configuration/configuration.ts";
import manifest from "@/fresh.gen.ts";

await start(manifest, startOptions);

We could write the start options straight into that file. However, we would need to duplicate them in our test files. To keep the test app as close as possible to the production app, we will define startOptions in a separate file, and import into main.ts and test files.

Create that configuration file (configuration/configuration.ts) with this content:

import type { StartOptions } from "$fresh/server.ts";

export const startOptions: StartOptions= {
  render: (ctx, render) => {
    ctx.lang = "en-GB";
    render();
  },
}

Update the language in line 5 to match your site language. See the separate video on the Deno Fresh HTML lang attribute for more on this.

🏡 Home Page Tests

Let’s add a tests folder to the project root directory for our end-to-end tests. You can add unit tests adjacent to the files they test (utils/network_test.ts for example).

We will have a test file in tests/routes for each route. Add a tests/routes/index_test.ts file for the index route with this content:

import { createHandler } from "$fresh/server.ts";
import type { ServeHandlerInfo } from "$fresh/server.ts";
import { load } from "$std/dotenv/mod.ts";
import { assertEquals, assertStringIncludes } from "$std/testing/asserts.ts";
import { startOptions } from "@/configuration/configuration.ts";
import manifest from "@/fresh.gen.ts";
import { DOMParser } from "deno-dom/deno-dom-wasm.ts";

await load({ envPath: ".env.test", export: true });

const url = "http://127.0.0.1:8001/";

const CONN_INFO: ServeHandlerInfo = {
  remoteAddr: { hostname: "127.0.0.1", port: 53496, transport: "tcp" },
};

Deno.test("Home route", async (t) => {
  const handler = await createHandler(manifest, startOptions);

  await t.step("it returns a 200 status code", async () => {
    const response = await handler(new Request(url), CONN_INFO);
    const { status } = response;
    assertEquals(status, 200);
  });
});

We have our first test! You can see that we mostly rely on JavaScript Web APIs, which will not be a surprise if you’ve used Deno before!

A few details are a little different, so let’s have a look at them:

  • We import environment variables in line 9. The envPath parameter lets you add a separate environment variable file (.env.test here) just for testing. Skip that parameter if you want to use .env instead. Adding the export: true option lets you access environment variables using Deno.env.get('SOME_SECRET') in your project files.
  • We use a Fresh server handler in line 18 to load the page content as the end user sees it.
  • Finally, we make an assertion in line 23, using Deno asserts (imported in line 4). See the Deno docs for a full list of available Deno assert functions.

Try running your tests from the Terminal with:

deno task test

🥱 More Home Page Tests

We mentioned accessing the DOM earlier. Let’s add a couple more tests to see this. We will:

  • check the HTML tag includes the lang attribute (<html lang="en-GB">); and
  • that all images have alt text, required for accessibility.

Update tests/routes/index_test.ts:

Deno.test("Home route", async (t) => {
  const handler = await createHandler(manifest, startOptions);

  await t.step("it returns a 200 status code", async () => {
    /* TRUNCATED... */
  });

  await t.step("it sets HTML lang attribute", async () => {
    const response = await handler(new Request(url), CONN_INFO);

    const body = await response.text();
    assertStringIncludes(body, `<html lang="en-GB">`);
  });

  await t.step("images have alt attribute", async () => {
    const response = await handler(new Request(url), CONN_INFO);
    const body = await response.text();
    const doc = new DOMParser().parseFromString(body, "text/html")!;
    const images = doc.getElementsByTagName("img");

    images.forEach((element) => {
      const altText = element.attributes.getNamedItem("alt")?.value;
      assertEquals(altText && typeof altText, "string");
    });
  });
});

The way we access the HTML body, using JavaScript Web APIs is probably already familiar to you. Then, DOMParser, again, uses standard APIs. Hopefully you included alt tags on your own page’s images, and these tests pass too!

🤖 API Route Tests

For the sake of completeness, let’s add a test for an API route. As you might expect, there is not much difference here.

We will write code to test the Joke API route included in Deno Fresh skeleton projects. It responds to GET requests with a joke in plain text.

Create tests/routes/api/joke_test.ts with this content:

import { createHandler } from "$fresh/server.ts";
import type { ServeHandlerInfo } from "$fresh/server.ts";
import { load } from "$std/dotenv/mod.ts";
import { assert, assertEquals } from "$std/testing/asserts.ts";
import { startOptions } from "@/configuration/configuration.ts";
import manifest from "@/fresh.gen.ts";

await load({ envPath: ".env.test", export: true });

const url = "http://127.0.0.1:8001/api/joke";

const CONN_INFO: ServeHandlerInfo = {
  remoteAddr: { hostname: "127.0.0.1", port: 53496, transport: "tcp" },
};

Deno.test("Jokes API route", async (t) => {
  const handler = await createHandler(manifest, startOptions);

  await t.step("it returns a joke", async () => {
    const response = await handler(new Request(url, {}), CONN_INFO);
    const { status } = response;
    assertEquals(status, 200);

    const body = await response.text();
    assert(body);
  });

});

Hopefully, that was easy to adapt to your own app.

🙌🏽 Deno Fresh Testing: Wrapping Up

We saw how you can set up Deno Fresh Testing in your own project. In particular, we saw:

  • how you can end-to-end test HTML pages on your Deno Fresh site;
  • how you make sure environment variables are defined in your tests; and
  • a way to test API routes.

The complete code for this post. I do hope the post has either helped you in testing an existing project or provided some inspiration for getting started with setting up TDD on a new project.

Get in touch if you have some questions about this content or suggestions for improvements. You can add a comment below if you prefer. Also, reach out with ideas for fresh content on Deno or other topics!

🙏🏽 Deno Fresh Testing: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, then please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter, @rodney@toot.community on Mastodon and also the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as Deno. Also, subscribe to the newsletter to keep up-to-date with our latest projects.

Did you find this article valuable?

Support Ask Rodney by becoming a sponsor. Any amount is appreciated!