2023-10-15
22 min read
Testing Fresh Components, Middleware and Handlers with fresh-testing-library
Table of contents
- Introduction
- Example Code
- Component Testing
- Middleware and Route Handler testing
- Conclusion
- Acknowledgements
Introduction
The fresh-testing-library
is a
new unit testing utility for Deno Fresh. Until now testing a Fresh application
used the built-in Deno test runner with Deno.test
and std/testing
utilities
for verifying low-level logic and the
Deno-Puppeteer or
Astral libs for end-to-end testing. The
fresh-testing-library
fills the niche between the other options to allow
isolated testing of fresh components, middleware and route handlers.
fresh-testing-library
creates a thin wrapper around the
Preact Testing Library
which is built upon the
DOM Testing Library.
All of them including fresh-testing-library
share an API under the
Testing Library moniker.
The fresh-testing-library
also adds utilities for testing Fresh route
handler's, route components and middleware. Those are under the server.ts
module while the component testing library code is in the component.ts
module.
Both can also be accessed via the mod.ts
module.
The Testing Library philosophy is to create tests that interact with the application the same way an app user would do. To do so, Testing Library tests hone in on verifying DOM elements.
Testing library also focusses on accessibility, offering a number of functions to find elements by accessible attributes.
The fresh-testing-library
is registered as a Deno third-party library under
the "https://deno.land/x/fresh_testing_library" URL.
Example Code
This blog post will focus on how to use the fresh-testing-library
. With that
in mind, I have created example code in several repos. They are:
- The repo for this blog: https://github.com/cdoremus/deno-blog/tree/main/tests (8 test files)
- The repo for the blog post I did on using signals with Fresh: https://github.com/cdoremus/fresh-todo-signals/tree/main/tests (6 test files)
- The web site components, middleware & route handling in the Fresh repo: https://github.com/cdoremus/fresh/tree/fresh-testing-lib/tests/www/ (15 test files)
Component testing
Setting up a fresh-testing-library component test
Using fresh-testing-library
for a component test requires the Deno test runner
and the testing/bdd.ts
module of the Deno standard library. The bdd
module
adds functions that are familiar to testing in Node.js using libraries like
Jest or Mocha. They include
describe
, it
, beforeAll
, afterAll
, beforeEach
and afterEach
.
Here is a simple annotated example of a fresh-testing-library
component test
that illustrates test setup and code:
// Adapted from Todo.test.tsx in Fresh
// signals blog post repo
import {
cleanup,
fireEvent,
render,
setup,
} from "https://deno.land/x/fresh_testing_library/component.ts";
import {
afterEach,
beforeAll,
describe,
it,
} from "https://deno.land/std/testing/bdd.ts";
import { assertEquals } from "https://deno.land/std/assert/mod.ts";
import Todo from "../islands/Todo.tsx";
describe("Todo.tsx test", () => {
// setup the jsdom environment
beforeAll(setup);
// unmount rendered component after each test
afterEach(cleanup);
it("should display todo...", () => {
// assign the component's text content via a prop
const text = "Foo";
// render a component's DOM markup
const { getByText } = render(<Todo text={text} index={1} />);
// find an HTML element using the rendered component's text content
const textElement = getByText(text);
// verify element's text content
assertEquals(textElement.textContent, text);
});
});
Using assertions for test verification
Functions in the assert
module of the Deno standard library works for fresh-testing-library
verifications. Anyone who has done any Deno testing will be familiar with them. However, in version 0.10.0 of the fresh-testing-library
the expect
function was added to components.ts
. Like others, this function comes from Preact Testing Library.
The expect
function has a number of matcher functions attached to it that do the verification. They have a Jasmine-like API. Some of them behave similarly to the Deno-native assert functions.
Here are some expect
function matchers and their Deno-native equivalent (if there is one):
expect function matcher | Deno-native assert |
---|---|
toBeInTheDocument() | assertExists()/assertEquals() |
toBe() | assertEquals() |
not.toBe() | assertNotEquals() |
toBeTruthy() | assert() |
toBeFalsy() | assertFalse() |
toBeEmptyDOMNode | [none] |
toBeRequired | [none] |
Here's a simple example how to use expect
with fresh-testing-library
:
import { expect } from "https://deno.land/x/fresh_testing_library/expect.ts";
it("should find 'Hello World' text in document", () => {
const { queryByText } = render(<div>Hello World</div>);
expect(queryByText("Hello World")).toBeInTheDocument();
// using getByText will error out before matchers are called
expect(queryByText("foobar")).not.toBeInTheDocument();
});
Note that you can use the not
matcher to negate a matcher that it calls.
The expect
function contains a number of matcher functions that check on function calls (like expect(foo).toBeCalled()
). To get them to work, you need to mock the functions. The jest-mock
library's fn
function was incorporated into the expect.ts
module in v0.11.0. You can also use fn
to mock return values. Here's a simple example how that works:
import { expect, fn } from "https://deno.land/x/fresh_testing_library/expect.ts";
it("should be able to use mock functions", () => {
const add = (num1: number, num2: number): number => num1 + num2;
expect(add(2,6)).toBe(8);
// create mock impl of add
const mockAdd = fn(add).mockImplementation((num1, num2) => num1 * num2);
const sum = mockAdd(2, 2);
expect(sum).toBe(4);
expect(mockAdd).toBeCalled();
expect(mockAdd).not.toBeCalledTimes(2);
expect(mockAdd(3, 5)).toBe(15);
expect(mockAdd).toBeCalledWith(3, 5);
});
If you need to mock a function that is a dependency of the component-under-test, send the mock function into the component as a prop. The real (non-mock) dependency would be sent in from the parent component so the component will work correctly at runtime.
The expect.test.tsx
file in this blog's repo has some examples of how to use expect
and fn
.
There are also other functions from the jest-mock
library incorporated into the expect.ts
module. They include mocked
, replaceProperty
, and spyOn
.
Running tests
Run fresh-testing-library
tests with this command line:
deno test --allow-read --allow-env
Both permission flags are required because the jsdom
library is used
internally in component tests. You might also need to add other permission flags
depending on the code you are testing.
Rendering components under test
The first thing you need in a fresh-testing-library
test is a call to the
library's render
function. It is used to instantiate the component-under-test.
Its first argument is required and is the JSX representation of a component
including its props.
import { render } from "https:/deno.land/x/fresh_testing_library/component.ts";
// renders component DOM wrapped inside a document.body
const screen = render(<Counter count={1} />);
The render
function returns an object that contains functions for verifying a
component's content. Instead of returning that object ( screen
in this case),
you can destructure the functions required to find the content. Here's an
example of a text search with getByText
destructured from render
:
import { render } from "https:/deno.land/x/fresh_testing_library/component.ts";
// renders component DOM wrapped inside a document.body
const { getByText } = render(<Counter cont={1} />);
const text = getByText("Hello World!");
The render
function has an optional second argument that is rarely used. For
more information, see the
RenderOptions
interface in the preact-testing-library
type documentation.
Finding DOM Elements
As stated previously, Testing Library libs including fresh-testing-library
focus on verifying DOM elements generated by the component-under-test. These
elements create the UI including interaction components.
Finder functions are found on the RenderResult
object that is returned from a
call to render
.
const screen: RenderResult = render(<Features />);
const text = screen.getByText("Hello World!");
Discovering DOM elements in a fresh-testing-library
test requires use of a
finder. Finder function names have three parts:
- The finder mechanism: 'get', 'query' or 'find'
- Is the search for single or multiple nodes: 'By' or 'AllBy'
- How to find an element: 'AltText', 'DisplayValue', 'LabelText', 'PlaceholderText', 'Role', 'TestId' or 'Title'
Combined, the first two parts of a finder function name indicates what the function returns. Here's how that works:
Finder function | Match Found returns | Match Not Found returns |
---|---|---|
getBy* | matching element | throws an error |
getAllBy* | matching element array | throws an error |
queryBy* | matching element | null |
queryAllBy* | matching element array | empty array |
findBy* | resolved Promise (element) | rejected Promise |
findAllBy* | resolved Promise (element array) | rejected Promise |
Also note that if you are using the singular 'getBy', 'queryBy' or 'findBy' functions and more than one element is returned, then an error is thrown. Conversely, a getAllBy, queryAllBy and findAllBy function returns an array with a single element if only one matching item exists.
I need to interject here that while the table above -- and other documentation -- points out that an element (HTMLElement) gets returned, if the element has child elements (like an HTML form), then the whole node gets returned. However, most of the time you'll want to fashion the finder function to find a single element.
The full name of a finder function specifies how an element is located. Here's what they are with a 'getBy' prefix (the same functions exist using 'queryBy' or 'findBy'):
getByAltText
- finds the element using thealt
attributegetByDisplayValue
- finds aninput
,textarea
orselect
element using thevalue
attribute.getByLabelText
- finds the element using thelabel
attribute. Obviously, this is mostly used to locate form elements wrapped in alabel
.getByPlaceholderText
- finds the element using theplaceholder
attribute found in atext
ortextarea
form element.getByRole
- finds the element by itsrole
attribute or implicit role (see below).getByTestId
- finds an element using thedata-testid
attributegetByTitle
- finds an element using the enclosedtitle
element. This is best used with an svg graphic that contains atitle
child element.
All of the finder functions have the same first two arguments. The first one is an HTML element which would be obtained from a previous call to a finder function. Most of the time you will not need this argument.
The second argument is used to match the the HTML element being searched for. It is either a number, string, regular expression or a function (see next section). The 'ByRole' method only takes specific string values (see below).
Finder functions have an optional third argument which is an options object. The properties of that argument is specific to the finder.
Text Argument Options
Most finder functions take a
TextMatch
which
can be:
- a string
- a regex
- a function
The TextMatch
function has optional string and HTMLElement
arguments and
returns a boolean (true
for a match; false
for no match). Here's an example
of a test with functional matching:
it("should show source code", () => {
const code = "console.log('Hello World')";
const { getByRole, getByText } = render(<CodeBox code={code} />);
// find code
const codeElement = getByRole("code");
// get the text content
const content = codeElement.textContent;
// Prism library breaks up code for styling purposes
assert(
getByText((content) => content.includes("console")),
);
assert(
getByText((content) => content.includes("log")),
);
assert(
getByText((content) => content.includes("Hello World")),
);
});
A finder function that takes a TextMatch
can contain an options object with an
exact
or normalization
property that affects the precision of the match.
There is a
section in the Testing Library docs that explores these options in detail,
but here they are in a nutshell:
exact
- (true
by default) determines whether the match is case-sensitive or not.normalization
- By default whitespace is collapsed when doing a text match. This property can be set to override that behavior.
Uses of get*, query* and find* Functions
As shown above, the first part of a finder function name indicates what gets returned when you call the function. For that reasons, each one of them has a favored use
get* - use to find elements to be able to verify their attributes or content or invoke an event. You will probably use this most of the time in a component test.
query* - should only be used to verify the existence or non-existence of an element. Otherwise, a 'get*' finder should be used.
For example, I have a test where I want to make sure that no Todo
components
will be shown. Each Todo
contains a button
element. I use queryAllByRole
to test for an empty array returned by that function. If I used getAllByRole
, an error would be thrown
if nothing was returned.
// TodoList.test.tsx in Fresh signals post code repo
it("should not display list of todos...", () => {
const todos = [] as string[];
state.todos.value = todos;
const { queryAllByRole } = render(
<AppState.Provider value={state}>
<TodoList />
</AppState.Provider>,
);
// queryAllBy returns an empty array; getAllBy just throws an error
const buttons = queryAllByRole("button");
assertEquals(buttons.length, 0);
});
- find* - use when an async operation or rendering delay would prevent an element or elements from appearing in the UI. Fetching remote data is an example. Another example is when an action causes a rerender. Here's how
findBy
is used:
// MenuLink.test.tsx in this blog's repo
it("Show About link", async () => {
// change the location, triggering a menu rerender
globalThis.window.location = { href: "/about" };
const { findByText } = render(<MenuLink />);
// findByText returns a Promise
const page = await findByText("Home");
const text = page.textContent;
assertExists(text);
assertNotEquals(text, "About");
});
- waitFor While not strictly a finder function,
waitFor
is used under the covers by the findBy* finder. However, it can be used standalone.
The
waitFor
function
is used for verifying DOM elements that take a while to render due to an async
operation. Here's how waitFor
is used:
await waitFor(() => {
assertEquals(counter.textContent, count.value.toString());
});
As shown, waitFor
is an async function that takes a function argument and
optionally an options argument. This function runs until the waited-for
operation succeeds or times out. The timeout is 1000ms by default, but that can
be changed in the options argument's timeout
property.
Using ByRole finder functions
Kent C. Dodds, the creator of Testing Library, recommends to
prefer the use of the 'ByRole' finder function to discover DOM nodes. This is a tough thing to do since role
options are not well documented in Testing Library docs. Instead, a role used by Testing Library is documented in the WAI ARIA accessibility standard.
The 'ByRole' finder requires a string that is a valid WAI ARIA role name (see this list of WAI ARIA roles).
Certain HTML elements have
an implicit role. Some of
these elements have the same role name as the element name. They include
button
, article
, code
and img
.
Nearly all of the HTML form elements also have implicit roles. This includes
input
elements with type="text"
(textbox
role), type="radio"
(radio
role), type="range"
(slider
role), type="number"
(spinbutton
role) and
type="submit"
(button
role). The textarea
element's implicit role is
textbox
while the select
element's implicit role is listbox
or combobox
.
Another handy implicit role to use is link
for an HTML anchor (a
tag), but the anchor must have a href
attribute. Here's an example of a component test to verify 3 links:
// Header.test.tsx in Fresh repo
describe("components/gallery/Header.tsx", () => {
beforeAll(setup);
afterEach(cleanup);
it("should show 3 links", () => {
const { getAllByRole } = render(<Header active="" />);
const links = getAllByRole("link");
assertEquals(links.length, 3);
});
});
When you want to use an implicit role, do not set an element's role
attribute.
Sometimes you might have multiple elements with the same implicit role; multiple buttons, for instance. In that case use the options argument.
Each role has a different set of options. All of them have a name
property,
but the property's value depends on the role. For the button
role, the name
property is the button's value. For the img
role, the name
property is the
value of the element's alt
attribute. Roles for HTML elements without an
implicit role must be specified using the role
attribute.
Many IDEs like Visual Studio Code support discovery of valid roles using
autocompletion. Just type getByRole("")
putting your cursor within the quotes. Then press ctrl/cmd-spacebar
and a list of valid roles should appear.
If you use an argument to a 'ByRole' finder that is illegal, when the test is
run the error message will point out the available implicit roles. Alternately,
you can use the fresh-testing-library
function logRole
to find the implicit
roles within your component-under-test.
Accessibility testing
Testing Library emphasizes accessibility by having a number of finder functions that use accessibility attributes. The 'ByRole' finder is a big one, but 'ByPlaceholderText', 'ByAltText', 'ByLabel' and 'ByTitle' are others.
Here is an example of using the title
attribute with the 'ByTitle' finder:
// Background.test.tsx in Fresh repo
it("should display background image", () => {
// Background takes any number of native attributes or Preact props
const { getByTitle } = render(<Background title="background" />);
const bg = getByTitle("background");
const style = bg.getAttribute("style");
assertEquals(style, "background-image: url(/gallery/grid.svg);");
});
Testing User Interactions
The fresh-testing-library
contains two ways for simulating user interactions:
fireEvent
and userEvent
.
The fireEvent
function is used for simple interactions with the UI, while
userEvent
simulated the subtile nature of the interactions. For instance when a
button is pressed, fireEvent.click
would cover the DOM click event, but
userEvent.click
encompasses the hover, keydown and keyup events in addition to
the click event. In other words, userEvent
more closely behaves the way users
would
(see this article for more details).
fireEvent
Using The fireEvent
object includes function properties for almost 90 DOM events
They include:
fireEvent.click
- to invoke anonClick
handlerfireEvent.change
- to invoke anonChange
handlerfireEvent.submit
- to invoke anonSubmit
handlerfireEvent.keyDown
- to invoke anonKeyDown
handler
See the EventType
union type in the TypeScript docs for an enumeration of
all events.
Each of the fireEvent
function properties takes an argument that is the
HTMLElement
event target.
Here's an example of using a 'click' event to invoke a button click:
// Counter.test.tsx in Fresh signals blog post
it("should display count and increment/decrement it correctly", async () => {
const count = signal<number>(9);
const { queryByRole, queryByText } = render(<Counter count={count} />);
const plusOne = queryByRole("button", { name: "+1" });
assertExists(plusOne);
const minusOne = queryByRole("button", { name: "-1" });
assertExists(minusOne);
await fireEvent.click(plusOne);
assertFalse(queryByText("9"));
assertExists(queryByText("10"));
await fireEvent.click(minusOne);
assertExists(queryByText("9"));
assertFalse(queryByText("10"));
});
In this case, the event target are two button elements, one for incrementing the count, the other for decrementing it.
userEvent
Using The userEvent
object has 16 function properties. They include clear
, click
,
copy
, cut
, dblClick
, deselectOptions
, hover
, keyboard
, paste
,
pointer
, selectOptions
, tab
, tripleClick
, type
, unhover
and
upload
. As you can surmise from the function names, these correspond to actual
ways that the user interacts with the UI rather than strict DOM events.
The arguments for these functions can be very different. Many take a DOM element like, click
, dblClick
and hover
, while others have very different arguments. See the userEvent
docs or use your IDE (hover over the function or right click and select "Go to Definition") to discover these args.
The userEvent
object has a setup
function that should be used to instantiate
the object before it is used. Here's what that looks like:
const user = userEvent.setup();
The previous fireEvent
example can be adapted to use userEvent
. Here is what
it looks like:
// Be sure to import userEvent from FTL component.ts
it("should display count and increment/decrement it correctly", async () => {
const user = userEvent.setup();
const count = signal<number>(9);
const { queryByRole, queryByText } = render(<Counter count={count} />);
const plusOne = queryByRole("button", { name: "+1" });
assertExists(plusOne);
const minusOne = queryByRole("button", { name: "-1" });
assertExists(minusOne);
await user.click(plusOne);
assertFalse(queryByText("9"));
assertExists(queryByText("10"));
await user.click(minusOne);
assertExists(queryByText("9"));
assertFalse(queryByText("10"));
});
While userEvent
seems like a more logical choice since it focuses on user
interactions, fireEvent
covers more event types so it needs to be used in
certain circumstances. For instance, fireEvent
has a change
function for
simulating an onChange
event, while userEvent
does not have a similar
function.
Testing component state management
Preact signals provide a means to do local and global state management with signals. A previous Craig's Deno Diary post demonstrated how to use signals with Fresh.
Testing local state
Compared to useState
, the signal
function only returns a single value and
not an array with a setter function. Updating the signal involves assigning the
value property of the signal a new value.
const count = signal<number>(0);
const newValue = count.value + 1;
count.value = newValue;
The local state often changes when an event gets invoked which in turn updates
the UI. In a fresh-testing-library
test, the fireEvent
function would
trigger his kind of change.
const [getByRole, queryByText] = render(<Counter />);
const button = getByRole("button");
fireEvent.click(button);
assertExists(queryByText("Count: 1"));
Testing global state
Global state management with Preact signals requires a module that holds that state and using the Preact context to pass the state to child components. I will not go over how this is coded in detail as you can refer the the Craig's Deno Diary post on how it's done.
The code used to illustrate the state management testing shown here is taken from the signals blog post's repo.
Testing a component that uses global state requires that you wrap the component
in a Preact context provider that passes the state into the component. The context is created in the parent component using the createContext
function:
// App.tsx
export const AppState = createContext<AppStateType>({} as AppStateType);
The AppState
context has a provider field used to pass the state to child components.
The child components discover the state using the useContext
hook. Here's a
component that displays a list obtained from the context:
// TodoList.tsx in Fresh signals blog post repo
import { useContext } from "preact/hooks";
import { AppState } from "./App.tsx";
import Todo from "./Todo.tsx";
export default function TodoList() {
const { todos } = useContext(AppState);
return (
<div className="todos">
{/* todos are a signal that contain the todos value */}
{todos.value?.map((item: string, i: number) => {
return <Todo text={item} index={i} />;
})}
</div>
);
}
To test a component in isolation, you need to wrap it in a context provider so that state can be passed to the child component. Here's what that kind of test looks like:
// TodoList.test.tsx in Fresh signals blog post repo
it("should display list of todos...", () => {
const todos = ["Foo", "Bar", "Baz"];
// state is imported from the module that holds it (state.ts)
state.todos.value = todos;
const { getAllByRole, queryByText } = render(
<AppState.Provider value={state}>
<TodoList />
</AppState.Provider>,
);
// verify that all todos are displayed
for (let i = 0; i < todos.length; i++) {
assertExists(queryByText(todos[i]));
}
// Each todo has a delete button
const buttons = getAllByRole("button");
assertEquals(buttons.length, 3);
});
Notice in the test that we assign the state's todos
value before we attached it to the AppState.Provider
context provider.
The
original blog post on Fresh signals
has been updated with fresh-testing-library
component tests including the test
used in this section.
Component testing troubleshooting tips
- Manipulating
IS_BROWSER
- This constant is used a lot in Fresh app code, but testing it can be problematic. In order to setIS_BROWSER
to false, you need to set thedocument
object toundefined
. Doing that will cause afresh-testing-library
test to fail because thejsdom
library cannot function without a validdocument
object. Therefore, testing code that usesIS_BROWSER
cannot be done withfresh-testing library
. debug
functions - used for printing out the DOM returned from callingrender
or any of the finder functions. The former prints out the complete DOM that was rendered and the latter prints out the returned DOM from a finder function call. Here's how to use them:
const { debug, queryByRole } = render(<MyComponent />);
// print out all the component's DOM elements wrapped in a body element
debug();
const element = queryByRole("button");
// prints out the button element's DOM
element.debug();
Middleware and Route Handler testing
Testing middleware
Testing middleware employs the createMiddlewareHandlerContext
function to mock
out the MiddlewareHandlerContext
used in a middleware handler. Here's what the
code and a test would look like for the
middleware in this repo;
// _middleware.ts in this blog's repo
import { MiddlewareHandlerContext } from "$fresh/server.ts";
export const handler = setCacheControlHeaders();
export function setCacheControlHeaders() {
return async (_req: Request, ctx: MiddlewareHandlerContext) => {
const resp = await ctx.next();
resp.headers.set("Cache-Control", "public, max-age=21600, immutable");
return resp;
};
}
// middleware.test.ts
import { assertEquals } from "std/assert/assert_equals.ts";
import { createMiddlewareHandlerContext } from "$fresh-testing-library";
import { setCacheControlHeaders } from "../../routes/_middleware.ts";
import manifest from "../../fresh.gen.ts";
Deno.test("should set Cache-Control header", async () => {
const req = new Request(`http://localhost:3000/`);
// @ts-ignore ignores "type ... is not assignable to type Manifest" error
const ctx = createMiddlewareHandlerContext(req, { manifest });
let resp = new Response();
const middleware = await setCacheControlHeaders();
await middleware(req, ctx);
resp = await ctx.next();
assertEquals(
resp.headers.get("Cache-control"),
"public, max-age=21600, immutable",
);
});
You can use Deno.test
in this case since a middleware test does not require
setup or cleanup.
The Request
object can be used to find out the current URL or request method
(GET, POST, etc) for middleware with logic that is more sophisticated than my
simple example.
Testing route handlers
At this point, route testing using fresh-testing-library
is confined to
testing handler logic and not the handler's Response
including verifying
response markup.
You use the createHandlerContext
function in fresh-testing-library
to test
the handler function of a route component.
// about.test.ts in this blog's repo
Deno.test("routes/About.tsx handler tests...", async (t) => {
await t.step("should respond to GET request in DEV...", async () => {
assert(handler.GET);
const req = new Request("http://localhost:8000/about");
// @ts-ignore manifest typing
const ctx = createHandlerContext(req, { manifest });
const res = await handler.GET(req, ctx);
assertEquals(res.status, 200);
});
// ... other tests here
});
You do not need startup
and cleanup
functions for handler tests, so we use
the Deno.test
runner function here instead of the bdd
module functions.
Alternatively, the Fresh codebase includes the createHandler
function for
testing a route handler. The createHandler
function creates a mock handler
function.
See the Fresh docs for more details on using this function.
Grouping routes was
added in Fresh v1.4. Version 0.8.0, of the fresh-testing-library
added support
for testing route groups. fresh-testing-library
route handler tests
transparently works if routes are grouped.
Testing async route components
Fresh async route components
are components where an async route handler function is inlined into the route
page component. In that case, the route page component is declared as
asynchronous (using the async
keyword). Here's what that look likes (taken
from the Fresh routes documentation):
export default async function MyPage(req: Request, ctx: RouteContext) {
const value = await loadFooValue();
return <p>foo is: {value}</p>;
}
Fresh also ships with a
defineRoute
function to simplify the creation of async route components.
A fresh-testing-library
test of an async route component uses the
createRouteContext
function in the server
module. Here's how it is used in a
fresh-testing-library
example:
import {
cleanup,
getByText,
render,
setup,
} from "$fresh-testing-library/components.ts";
import { createRouteContext } 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";
describe("routes/users/[id].tsx", () => {
beforeAll(setup);
afterEach(cleanup);
it("should work", async () => {
const req = new Request("http://localhost:8000/users/2");
const ctx = createRouteContext<void>(req, { manifest });
const screen = render(await UserDetail(req, ctx));
const group = screen.getByRole("group");
assertExists(queryByText(group, "bar"));
});
});
In this case, UserDetails
is the async route component.
Conclusion
Use the example code given at the beginning of this blog post
to guide you through the the core fresh-testing-library
concepts introduced
above. There are over two dozen test files for you to peruse in those three
repos.
There are also examples in the
fresh-testing-library
repo.
Check out the components.test.tsx
, server.test.tsx
and routes.test.tsx
files used to test code in the demo
folder.
Also be aware that fresh-testing-library
has not yet released version 1.0, so
it will continue to evolve to include more features and bug fixes. In addition,
I know by first-hand experience that the fresh-testing-library
author is very
responsive to issues submitted to the library's repo.
Finally, since the component testing feature of fresh-testing-library
wraps
the Preact Testing Library, you need to be familiar with that
library's documentation.
It provides a guide to the full component testing API.
Acknowledgements
Special thanks goes to fresh-testing-library
author Yuki Tanaka (@uki00a) for
answering my many questions, promptly fixing bug reports that I submitted and
reviewing a draft of this article. I would also like to thank Kyle June (@kylejune) for his help with understanding the limitations of mocking with expect
.