Testing
Wasp provides helpers for testing your client code with Vitest.
Testing server code
Wasp doesn't provide helpers for testing server code yet. You can follow the progress in this GitHub issue.
Setting up client tests
Installing the packages
Install Vitest and the libraries for testing React components:
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom msw
vitest: the test runner.jsdom: a browser environment for your component tests.@testing-library/react: renders React components and finds elements in them.@testing-library/jest-dom: adds matchers for checking the page, liketoBeInTheDocument().msw: mocks HTTP requests. The Wasp helpers use it to mock your queries and APIs.
Configuring Vitest
Vitest reads its configuration from your vite.config.ts, so your tests use the same Vite setup as your app. Add a test section to it, built with Wasp's waspTestConfig helper:
- JavaScript
- TypeScript
import { defineConfig } from "vitest/config";
import { wasp } from "wasp/client/vite";
import { waspTestConfig } from "wasp/test";
export default defineConfig({
plugins: [wasp()],
test: waspTestConfig({
setupFiles: ["./src/test/setup.ts"],
}),
});
import { defineConfig } from "vitest/config";
import { wasp } from "wasp/client/vite";
import { waspTestConfig } from "wasp/test";
export default defineConfig({
plugins: [wasp()],
test: waspTestConfig({
setupFiles: ["./src/test/setup.ts"],
}),
});
waspTestConfig sets the Vitest options a Wasp app needs, so Vitest:
- runs the test files in your
srcfolder (e.g.,TaskList.test.tsxorhelpers.spec.js), - runs them in
jsdom, a simulated browser, so your components can render, - gives your client code a placeholder server URL, since your tests mock the server instead of running it.
The options you pass to waspTestConfig are regular Vitest options, and they override Wasp's. Here, setupFiles runs src/test/setup.ts before each test file. Create it:
- JavaScript
- TypeScript
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
afterEach(() => {
cleanup();
});
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
afterEach(() => {
cleanup();
});
It prepares every test file in two ways:
- It adds matchers for checking the page, like
expect(button).toBeInTheDocument(). - It removes what a test rendered when the test ends, so the next test starts with an empty page.
Running your tests
Add a script for running your tests to your package.json:
{
"scripts": {
"test": "vitest"
}
}
Your tests import code from the Wasp SDK, which Wasp generates when it compiles your app.
Keep wasp start running in one terminal, and run your tests in another:
npm test
Vitest watches your files and reruns the affected tests when you change them. In CI, it runs the tests once instead. There, generate the SDK with wasp compile before running the tests:
wasp compile
npm test
Running tests visually
Vitest has a UI that shows your test results in the browser. Install it and start Vitest with it:
npm install -D @vitest/ui
npm test -- --ui
Writing client tests
Testing plain functions
Functions that don't render components or call your server (through queries, actions, or APIs) need no extra setup. Import them and test them like in any other project:
- JavaScript
- TypeScript
export function areThereAnyTasks(tasks) {
return tasks.length !== 0;
}
import type { Task } from "wasp/entities";
export function areThereAnyTasks(tasks: Task[]): boolean {
return tasks.length !== 0;
}
- JavaScript
- TypeScript
import { expect, test } from "vitest";
import { areThereAnyTasks } from "./helpers";
test("areThereAnyTasks", () => {
expect(areThereAnyTasks([])).toBe(false);
});
import { expect, test } from "vitest";
import { areThereAnyTasks } from "./helpers";
test("areThereAnyTasks", () => {
expect(areThereAnyTasks([])).toBe(false);
});
Testing components
Wasp wraps every page of your app in providers: React Query for queries and actions, and React Router for links. A component that uses queries, actions, or links fails to render without them.
renderInContext from wasp/client/test renders a component inside the same providers:
- JavaScript
- TypeScript
import { Link } from "wasp/client/router";
export function TasksHeader() {
return (
<div>
<h1>Your tasks</h1>
<Link to="/">Back home</Link>
</div>
);
}
import { Link } from "wasp/client/router";
export function TasksHeader() {
return (
<div>
<h1>Your tasks</h1>
<Link to="/">Back home</Link>
</div>
);
}
- JavaScript
- TypeScript
import { screen } from "@testing-library/react";
import { expect, test } from "vitest";
import { renderInContext } from "wasp/client/test";
import { TasksHeader } from "./TasksHeader";
test("links back home", () => {
renderInContext(<TasksHeader />);
expect(screen.getByRole("link", { name: "Back home" })).toBeInTheDocument();
});
import { screen } from "@testing-library/react";
import { expect, test } from "vitest";
import { renderInContext } from "wasp/client/test";
import { TasksHeader } from "./TasksHeader";
test("links back home", () => {
renderInContext(<TasksHeader />);
expect(screen.getByRole("link", { name: "Back home" })).toBeInTheDocument();
});
Mocking your queries
A component that calls useQuery sends a request to your server. To test the component without running the server, mock the query's response.
Call mockServer once at the top of your test file. It starts a mock server for the file and returns mockQuery, which takes a query and the data it should return:
- JavaScript
- TypeScript
import { getTasks, useQuery } from "wasp/client/operations";
export function TaskList() {
const { data: tasks } = useQuery(getTasks);
return (
<ul>
{tasks?.map((task) => (
<li key={task.id}>
<input type="checkbox" checked={task.isDone} readOnly />
{task.description}
</li>
))}
</ul>
);
}
import { getTasks, useQuery } from "wasp/client/operations";
export function TaskList() {
const { data: tasks } = useQuery(getTasks);
return (
<ul>
{tasks?.map((task) => (
<li key={task.id}>
<input type="checkbox" checked={task.isDone} readOnly />
{task.description}
</li>
))}
</ul>
);
}
- JavaScript
- TypeScript
import { screen } from "@testing-library/react";
import { expect, test } from "vitest";
import { getTasks } from "wasp/client/operations";
import { mockServer, renderInContext } from "wasp/client/test";
import { TaskList } from "./TaskList";
const { mockQuery } = mockServer();
test("shows the tasks", async () => {
mockQuery(getTasks, [
{ id: 1, description: "Buy milk", isDone: true, userId: 1 },
]);
renderInContext(<TaskList />);
await screen.findByText("Buy milk");
expect(screen.getByRole("checkbox")).toBeChecked();
});
import { screen } from "@testing-library/react";
import { expect, test } from "vitest";
import { getTasks } from "wasp/client/operations";
import { mockServer, renderInContext } from "wasp/client/test";
import { TaskList } from "./TaskList";
const { mockQuery } = mockServer();
test("shows the tasks", async () => {
mockQuery(getTasks, [
{ id: 1, description: "Buy milk", isDone: true, userId: 1 },
]);
renderInContext(<TaskList />);
await screen.findByText("Buy milk");
expect(screen.getByRole("checkbox")).toBeChecked();
});
mockServer resets the mocks after each test, so every test mocks the data it needs.
Mocking your APIs
If your component calls one of your APIs, mock the API with mockApi. It takes the API's HTTP method and path, and the JSON it should respond with:
- JavaScript
- TypeScript
import { useEffect, useState } from "react";
import { api } from "wasp/client/api";
export function TaskStats() {
const [doneTasks, setDoneTasks] = useState();
useEffect(() => {
api
.get("/stats")
.json()
.then((stats) => setDoneTasks(stats.doneTasks));
}, []);
return <p>{doneTasks} tasks done</p>;
}
import { useEffect, useState } from "react";
import { api } from "wasp/client/api";
export function TaskStats() {
const [doneTasks, setDoneTasks] = useState<number>();
useEffect(() => {
api
.get("/stats")
.json<{ doneTasks: number }>()
.then((stats) => setDoneTasks(stats.doneTasks));
}, []);
return <p>{doneTasks} tasks done</p>;
}
- JavaScript
- TypeScript
import { screen } from "@testing-library/react";
import { test } from "vitest";
import { HttpMethod } from "wasp/client";
import { mockServer, renderInContext } from "wasp/client/test";
import { TaskStats } from "./TaskStats";
const { mockApi } = mockServer();
test("shows the stats", async () => {
mockApi({ method: HttpMethod.Get, path: "/stats" }, { doneTasks: 3 });
renderInContext(<TaskStats />);
await screen.findByText("3 tasks done");
});
import { screen } from "@testing-library/react";
import { test } from "vitest";
import { HttpMethod } from "wasp/client";
import { mockServer, renderInContext } from "wasp/client/test";
import { TaskStats } from "./TaskStats";
const { mockApi } = mockServer();
test("shows the stats", async () => {
mockApi({ method: HttpMethod.Get, path: "/stats" }, { doneTasks: 3 });
renderInContext(<TaskStats />);
await screen.findByText("3 tasks done");
});
API reference
waspTestConfig »
Configure Vitest with the defaults a Wasp app needs.
renderInContext »
Render a component with the providers Wasp needs.
mockServer »
Set up a mock server and mock your Queries and APIs.
WaspTestWrapper »
Wrap your own Testing Library renders in the providers Wasp needs.