- Lab
- Core Tech

Guided: Testing a Next.js Finance Application with Vitest
In this Guided Code Lab, you will create tests and mocks using an increasingly popular testing framework, Vitest. You will be provided an existing finance application that is built using Next.js and Prisma. You will be tasked with writing unit tests for a client component, server component, and server actions that perform CRUD operations using Prisma. By the end of this lab, you will have a deeper understanding of Vitest and how it can and cannot be used to unit test a full stack application.

Path Info
Table of Contents
-
Challenge
Introduction
Welcome to the Guided: Testing a Next.js Finance Application with Vitest lab.
Throughout this lab, you will become familiar with how to use Vitest to test a Next.js application. This lab will cover how to configure Vitest, setup a testing script, and unit test a full stack Next.js finance application. You will learn about Vitest’s strengths and limitations all while completing tests for a Next.js client component, server component, and server action. You will also learn how to use Vitest to mock modules and their associated methods.
To start, you have been given a Next.js finance application. You can launch the application by entering
npm run dev
in the Terminal. The application has a Home page that displays all transactions in a table and highlights expense transactions in red and income transactions in green. There are also Income and Expenses pages where you can create, edit, and delete the corresponding transactions. Lastly, there is a Plan page where you can create, edit, and delete budget plans.The application’s main pages (e.g. Home, Expenses, Income, and Plan) are all Next.js server components. A server component is one where the backend compiles the HTML and hands that to the frontend instead of just supplying data. These server components render client components. Client components are what you are familiar with if you have used React in the past. They are generated entirely on the frontend within the browser.
The application is using a SQLite database that is manipulated using Prisma. Prisma is a modern database toolkit designed for TypeScript and JavaScript applications. It simplifies database access by providing a type-safe query builder, an Object-Relational Mapping (ORM) layer, and a schema definition language. This lab will cover how to mock a
Prisma Client
using Vitest to unit test Next.js server actions.You will start by configuring the Next.js application to use Vitest. Then, you will be responsible for testing the
TransactionTable
client component. This will use Vitest alongside React Testing Library to create unit tests for theTransactionTable
component. You will verify that transaction data exists in the table and the transactions link to their corresponding edit pages. This section will feel familiar if you have experience testing frontend code using a React Testing Library and Jest.Next, you will test the
ExpensesPage
server component. You will learn about the limitations of Vitest when it comes to testingasync
server components and how to mock other client component modules that are being rendered from theExpensesPage
component.Lastly, you will mock the Prisma’s
Prisma Client
module and test transactions CRUD server actions. This will cover what a deep mock is from thevitest-mock-extended
library and why it is important when testing results from server actions.You do not need any previous experience with Next.js, Prisma, Vitest, or Vitest Mock Extended to complete this lab. Any specifics you will need around these technologies will be provided to help you complete the step at hand. It will benefit you to have basic experience with Javascript, JSX, and React. Knowledge of some testing framework like Jest and React Testing Library would also be helpful for this lab, but it is not required. Throughout each step, you will learn about what is available to you through Vitest’s Test API and React Testing Library.
There is a
solution
directory with correspondingstep
directories that you can refer to if you are stuck or want to check your implementations at any time. Keep in mind that the solution provided in this directory is not the only solution, so it is acceptable for you to have a different solution so long as the test functions as intended. The solution provided will be for the entire step.
Activity - Start the Application
Launch the application by entering
npm run dev
in the Terminal. The application can be seen in the Web Browser tab atlocalhost:3000
. Take a moment to become familiar with the different pages. You will be testing the Expenses page and the transaction table that appears on it as well as the transactions CRUD operations. The starting application should look like the picture below:Remember: Throughout the lab, any code changes within the
app
directory and the database will be reflected live on the website after refreshing, as long as the application is running. If you do, however, want to stop and restart the application, you can do so by pressingCtrl+C
in the Terminal and rerunningnpm run dev
. This will hold true for when you are running tests as well in the future. Tests will update when changes are made without needing to be restarted. -
Challenge
Setting up Vitest
Vitest Overview
Testing application code has become a very important step in creating developers that are confident in the code they write and refactor. It can help developers create software more efficiently. Vitest is a testing framework that provides a set of tooling to build and run tests, as well as create mocks of modules. Vitest is designed to assist developers when they are writing tests for their applications. Vitest can be used alongside the React Testing Library to test frontend components. Vitest supports mocking modules of all kinds. Mocks are helpful when a developer does not want certain code to be executed. One reason a developer may not want code to be executed is because it interacts with a database that may not exist in the testing environment - as is the case of this application. Another reason Vitest mocks can be helpful is the ability to separate concerns and not test the same internal module twice. Mocks will be discussed further in a future step.
Setting up Vitest
Vitest has already been installed for you using Node Package Manage,
npm
. If you are usingnpm
in another project, Vitest is installed by running the following command:npm install -D vitest
. This application will also use the@vitejs/plugin-react
,vitest-mock-extended
,jsdom
, and@testing-library/react
libraries, so these libraries have also been installed for you using thenpm install -D @vitejs/plugin-react jsdom @testing-library/react vitest-mock-extended
command.However, configuring an application to be tested using Vitest requires you to do more than just installing the necessary libraries. In the activities below, you will have the opportunity to configure Vitest.
Activity 1 - Configuration
Edit the existing
vitest.config.js
file to configure Vitest as follows:- Add a
test
property in yourdefineConfig
parameter object. - Set the
test
property to be an empty object. - Inside the empty object, create an
environment
property. - Set the
environment
property to be”jsdom”
.- This lab uses
jsdom
as theenvironment
, but if you ever wanted to use a differentenvironment
this is where you would set it.
- This lab uses
- Add a
plugins
property at the same level as thetest
property. - Set the
plugins
property to be an empty list. - Inside the empty list, call the already imported
react
module.- This lab uses
vitest
’s react plugin which is why this step is necessary, but you could configure Vitest to not use any plugins by removing this property from the configuration.
- This lab uses
Example Vitest Configuration
import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], test: { environment: "jsdom", }, });
The
vitest.config.js
file is used to override the configuration ofvite.config.js
(if there is any) and configure Vitest. The finance application at hand does not rely on Vite specifically, which is why thevitest.config.js
file is used to configure vitest instead ofvite.config.js
.
Activity 2 - Test Script
Next, add a script named
test
that simply runs the commandvitest
inside yourpackage.json
file. This script will be responsible for running your tests from the Terminal with the following command:npm run test
.Example
package.json
{ "name": "finance-app", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "seed": "node seedDatabase/seed.js", "test": "vitest" ← You should have added this line }, … }
Now, run
npm run test
in the Terminal to run your application’svitest
tests. Vitest will watch for changes in your project by default and reruns files that have been changed or contain failing tests and/or failing test suites. Outside of this lab environment, you would not need to restart this command whenever you make changes to test and production files. However, in this lab, you will be asked to restart the tests at the end of each step to make sure all test files are compiled and ran (instead of just those that have been changed or are still failing). This is so you can compare test output against all files to what is expected.When you run
npm run test
, all test files are ran. Each test file is considered its own test suite. Test suites are designed to be a group of tests that relate to each other or test a certain context of the application. Test suites are created for every test file anddescribe
block. Currently, the application has six test suites (made up of three test files and threedescribe
blocks). There are no tests within any of these test suites. This means that you will notice that when you run the tests now all six test suites fail with an error that contains,"Error: No test suite found in ..."
.Run
npm run test
in Terminal, and your output should match the screenshot below: - Add a
-
Challenge
Test the `TransactionTable` Client Component
Introduction
In this step, you will be implementing the tests found in the
__tests__/(ui)/components/transactionTable.test.jsx
directory.Vitest links this test file with the component found in the
app/(ui)/components/transactionTable.jsx
directory. This is because Vitest can recognize that the two files are named the same, with the exception of the files' paths and that one file has.test
before the file extension.Throughout this step, you will become familiar with different Vitest API options (e.g.
expect
,test
,describe
,beforeEach
, andafterEach
). You will also userender
,screen
, andcleanup
from the React Testing Library to test theTransactionTable
client component.The test file you have been given to test the
TransactionTable
client component already imports theTransactionTable
component and other methods or modules you will use. This includes all imports you will need fromvitest
and@testing-library/react
to complete this step.It may be helpful to also look at the
app/lib/transactions/data/mockData.json
file to familiarize yourself with how transaction records will look as the frontend consumes them. This file will be used to provide test data for your tests and is being read in thegetTransactions
method already given to you in the__tests__/(ui)/components/transactionTable.test.jsx
file.
Activity 1 - Create the
TransactionTable
Component’s Test SuiteTest suites can be created implicitly (with a test at the top level of a test file) or explicitly using Vitest’s
describe
. By usingdescribe
you can define a new suite, in the current context, as a set of related tests or benchmarks and other nested suites. A suite lets you organize your tests and benchmarks so reports are more clear.describe
takes two parameters, a name for the test suite and a function that will contain test setup, test teardown, and tests for the test suite. You want to create a test suite that describes the tests for theTransactionTable
client component.- Under the
getTransactions
method, start adescribe
block. - Name the test suite
”Transaction Table client component”
. - For now have an empty test suite by passing
() => {}
as the second parameter.
Example
TransactionTable
describe
blockdescribe("Transaction Table client component", () => {});
Activity 2 - Before Each Test in the
TransactionTable
Test Suite Render theTransactionTable
Vitest’s
beforeEach
is used to set up a testing suite.beforeEach
registers a callback to be called before each of the tests in the current context runs.beforeEach
takes a function as a parameter to run as the callback. Set up the test suite you created in the first activity to use the React Testing Library’srender
to render theTransactionTable
before each test.- In the
describe
block from the first activity, callbeforeEach
. - Pass the
beforeEach
method as an asynchronous function lambda. - Inside the lambda, assign a variable called
transactions
to what is returned fromgetTransactions()
:await
the result ofgetTransactions
.- Define the
transactions
variable without assigning it to anything at the top of thedescribe
block.
- Call
render
with theTransactionTable
component withtransactions
for thetransactions
prop.
Example
TransactionTable
describe
block withbeforeEach
blockdescribe("Transaction Table client component", () => { let transactions; beforeEach(async () => { transactions = await getTransactions(); render(<TransactionTable transactions={transactions} />); }); });
Activity 3 - After Each Test Cleanup the
TransactionTable
ComponentVitest’s
afterEach
method works similarly tobeforeEach
except it registers a callback function to be called after each test is executed. You want to tear down the component to be rerendered again after each test.- Under the
beforeEach
block, callafterEach
. - Pass a function that calls React Testing Library’s
cleanup
method.
Example
TransactionTable
describe
block withafterEach
blockdescribe("Transaction Table client component", () => { let transactions; beforeEach(async () => { transactions = await getTransactions(); render(<TransactionTable transactions={transactions} />); }); afterEach(() => { cleanup(); }); });
Activity 4 - Assert that the
TransactionTable
Component Renders Transaction DataVitest’s
test
method defines a set of related expectations. It receives the test name and a function that holds the expectations to test. Vitest’sexpect
is used to create assertions. In this context,assertions
are functions that can be called to assert a statement. Vitest provides Chai assertions by default and also Jest compatible assertions built on top of Chai.You will now write your first Vitest
test
and useexpect
to assert that certain text appears on the React Testing Library’sscreen
.- Under the
afterEach
block, call Vitest’stest
. - Name the test
”displays each transaction's data in a table”
. - Pass a lambda function for the second parameter.
- Inside the lambda function iterate over every
transaction
using Javascript’sforEach
method on thetransactions
array. - Use Vitest’s
expect
to assert thatscreen.getByText(transaction.notes)
is defined.
Example
TransactionTable
describe
block with firsttest
blockdescribe("Transaction Table client component", () => { let transactions; beforeEach(async () => { transactions = await getTransactions(); render(<TransactionTable transactions={transactions} />); }); afterEach(() => { cleanup(); }); test("displays each transaction's data in a table", () => { transactions.forEach((transaction) => { expect(screen.getByText(transaction.amount)).toBeDefined(); }); }); });
At this point, you have written a test to assert that each transaction's amount is rendered on the
TransactionTable
client component. If you wanted to continue to extensively test theTransactionTable
component, you could also verify that the transaction’s category, date, and notes are rendered correctly. You may also create another test to verify that each transaction’s Edit button links to the correct page.If you still have
npm run test
running from the previous step, you will notice that it updates live and reruns some of the test suites/files again as you were making changes. However, it usually does not show the output for all of tests it can find for the application. It will zero in on the file being edited and any other failing test files, suites, or tests. It will not show you what other tests outside of the file most recently saved still pass. If you leave it running, you may see an output like below:For the rest of this lab, please quit your test execution in Terminal by typing
q
and then rerun thenpm run test
command. This will allow you to precisely compare your output to what is provided as an example throughout the rest of the lab and continue to verify that tests you implemented in previous steps still work.Now, please quit any test execution you have running and rerun
npm run test
. When this command completes, the output should look like:You should now see that one test file and one test is passing. There are still two test files that are failing. You will implement the failing test files in future steps. You can also see that only five test suites are failing, meaning the one you wrote in this step, titled
Transaction Table client component
is passing. This is because of the test you wrote in this step. This step is in theTransaction Table client component
test suite and the__tests__/(ui)/components/transactionTable.test.jsx
test file.
Note: You may have seen a
Warning: Invalid value for prop
actionon <form> tag.
message in the output. This is because testing a server component using Vitest is not fully supported as will be discussed in the next step. If you were to install thecanary
version of React this error would go away, but given it is still experimental, this lab does not run off of thecanary
version of React. - Under the
-
Challenge
Test the `ExpensesPage` Server Component
Introduction
In this step, you will be implementing the tests found in the
__tests__/(ui)/expenses/page.test.jsx
file.Vitest associates this test file with the component found in the
app/(ui)/expenses/page.jsx
file.Throughout this step, you will use the Vitest methods you learned about in the previous step (e.g.
expect
,test
,describe
,beforeEach
, andafterEach
). You will userender
,screen
, andcleanup
from the React Testing Library like in the previous step to test theExpensesPage
server component. In this step, you will learn about Vitest’svi
utility to help you create mocks on modules.The test file you have been given to test the
ExpensesPage
server component already imports theExpensesPage
component and all other methods or modules you will use to complete this step.The
ExpensesPage
testing suite has already been created for you within thedescribe
block with the name”async Expenses server component”
. Add all tests and test set up inside this test suite. You have also been given a few helpful variables to use within this test suite.
Activity 1 - Before Each Test in the
ExpensesPage
Test Suite Render theExpensesPage
Vitest is capable of testing client components just like any other framework. Vitest (as well as any other testing framework) is limited by how it can be used to test a server component since
async
server components are new to the React ecosystem. For this reason, you will often see end-to-end (E2E) tests usingplaykit
to test server components. In this step, you will test theasync
ExpensesPage
server component similar to how you would test a client component. You willawait
the rendered component in order to perform assertions on the component.Note: If you were to render the server component without
await
ing the result, you would get the following error when running your tests:[Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.]
The
beforeEach
call is used to set up a testing suite.beforeEach
registers a callback before each of the tests in the current context runs.beforeEach
takes a function as a parameter to run as the callback. Set up the test suite you created in the first activity to use the React Testing Library’srender
to render theTransactionTable
before each test.- In the
ExpensesPage
describe
block, callbeforeEach
. - Pass the
beforeEach
method as an asynchronous function lambda. - Inside the lambda, create a constant variable called
Expenses
and assign it to what is returned fromExpensesPage()
:await
the result ofExpensesPage
since it is an asynchronous server component.
- Call
render
and pass itExpenses
.
Example
ExpensesPage
describe
block withbeforeEach
blockdescribe("async Expenses server component", async () => { const transactions = await getTransactions(); const transactionsIdsString = transactions .map((transaction) => transaction.id) .join(" "); beforeEach(async () => { const Expenses = await ExpensesPage(); render(Expenses); }); });
Note: The difference between testing a server component and a client component with Vitest is that you must
await
the server component before you render it in the test. A client component is not asynchronous so this step is not necessary.
Activity 2 - Write a Test to Assert that the
ExpensesPage
Component Renders a Header- Under the
afterEach
block, call Vitest’stest
. - Name the test
”renders a header”
. - Pass a lambda function for the second parameter.
- Inside the lambda function, call
screen.getByText("Expenses");
.- Note: You did not have to wrap
getByText
in anexpect
block to create an assertion. This is because technically this will throw an error when it does not find anything with that text on the screen. This makes it so you do not have to wrap it in anexpect
block to see if it is defined if you choose not to.
- Note: You did not have to wrap
Example
ExpensesPage
describe
block with firsttest
blockdescribe("async Expenses server component", async () => { const transactions = await getTransactions(); const transactionsIdsString = transactions .map((transaction) => transaction.id) .join(" "); beforeEach(async () => { const Expenses = await ExpensesPage(); render(Expenses); }); afterEach(() => { cleanup(); }); test("renders a header", () => { screen.getByText("Expenses"); }); });
--- ## Activity 3 - Mock the
TransactionTable
ComponentVitest provides utility functions to help you out through its
vi
helper.vi.mock
allows you to mock modules and change the functionality of the module in the test.The
ExpensesPage
server component renders theTransactionTable
server component. You want to verify that theTransactionTable
is rendered with another test without knowing the specifics of what theTransactionTable
renders (also known as separation of concerns), this is where mocking steps in.You will mock the
TransactionTable
component in theapp/(ui)/components/createTransactionForm.jsx
file using Vitest’svi
utility. The component will now simply return a string of transaction ids separated by spaces.- Above the
describe
block, callvi.mock
. - Pass the relative path to the
TransactionTable
component,”../../../app/(ui)/components/transactionTable”
asvi.mock
’s first parameter. - Pass a lambda function for the second parameter.
- The lambda function should return an object with the property
default
.- Note: This object that is returned will essentially become the signature for the mocked module. Since
TableTransaction
was a default export and not a named export, it is under thedefault
property. In a future activity, you will have the chance to mock a module that is a named export.
- Note: This object that is returned will essentially become the signature for the mocked module. Since
- The value of the
default
property is another lambda function that takesprops
as a parameter and returnsprops.transactions.map((transaction) => transaction.id).join(" ");
.
Example
ExpensesPage
test fileimport { expect, test, describe, beforeEach, afterEach, vi } from "vitest"; import { render, screen, cleanup } from "@testing-library/react"; import ExpensesPage from "../../../app/(ui)/expenses/page"; import TRANSACTION_TYPE from "../../../app/utils/transactionTypeEnum"; import { promises as fs } from "fs"; async function getTransactions() { const file = await fs.readFile( process.cwd() + "/app/lib/transactions/data/mockData.json", "utf-8" ); const transactions = JSON.parse(file); return transactions.slice(0, 3); } vi.mock("../../../app/(ui)/components/transactionTable", () => ({ default: (props) => { return props.transactions.map((transaction) => transaction.id).join(" "); }, })); describe("async Expenses server component", async () => { const transactions = await getTransactions(); const transactionsIdsString = transactions .map((transaction) => transaction.id) .join(" "); beforeEach(async () => { const Expenses = await ExpensesPage(); render(Expenses); }); afterEach(() => { cleanup(); }); test("renders a header", () => { screen.getByText("Expenses"); }); });
Activity 4 - Keep Track of Props Passed to the
TransactionTable
Component by Spying on a Functionvi.fn
allows you to spy on functions and objects.vi.fn
can be initialized with a function or without one. Every time a function is invoked, it stores its call arguments, returns, and instances. Also, you can manipulate its behavior with methods. If no function is given, mock will return undefined when invoked.You will create a mock and spy on a function with
vi.fn
in this activity.- Above the
vi.mock
method call, initialize a constant variable titledmockTransactionTable
and assign it tovi.fn()
. - Inside the
default
value lambda function, callmockTransactionTable(props)
before returning the string of ids.
This will allow you to see what props were passed to
TransactionTable
when it was rendered byExpensesPage
in a future test. This is possible because of the spy on behaviorvi.fn
provides.Example
ExpensesPage
test fileimport { expect, test, describe, beforeEach, afterEach, vi } from "vitest"; import { render, screen, cleanup } from "@testing-library/react"; import ExpensesPage from "../../../app/(ui)/expenses/page"; import TRANSACTION_TYPE from "../../../app/utils/transactionTypeEnum"; import { promises as fs } from "fs"; async function getTransactions() { const file = await fs.readFile( process.cwd() + "/app/lib/transactions/data/mockData.json", "utf-8" ); const transactions = JSON.parse(file); return transactions.slice(0, 3); } const mockTransactionTable = vi.fn(); vi.mock("../../../app/(ui)/components/transactionTable", () => ({ default: (props) => { mockTransactionTable(props); return props.transactions.map((transaction) => transaction.id).join(" "); }, })); describe("async Expenses server component", async () => { const transactions = await getTransactions(); const transactionsIdsString = transactions .map((transaction) => transaction.id) .join(" "); beforeEach(async () => { const Expenses = await ExpensesPage(); render(Expenses); }); afterEach(() => { cleanup(); }); test("renders a header", () => { screen.getByText("Expenses"); }); });
--- ## Activity 5 - Verify
TransactionTable
Is Rendered byExpensesPage
Using a Test- Under the
afterEach
block, call Vitest’stest
. - Name the test
”renders a transactions table”
. - Pass a lambda function for the second parameter.
- Inside the lambda function, call
screen.getByText(transactionsIdsString);
. - Inside the lambda function, assert that
mockTransactionTable
was called with an object that looks like{ transactions: transactions }
.- Note: You can call
toHaveBeenCalledWith
on any Vitestexpect
assertion. - Hint: Pass
toHaveBeenCalledWith
expect.objectContaining({ transactions: transactions })
.
- Note: You can call
Example
ExpensesPage
test fileimport { expect, test, describe, beforeEach, afterEach, vi } from "vitest"; import { render, screen, cleanup } from "@testing-library/react"; import ExpensesPage from "../../../app/(ui)/expenses/page"; import TRANSACTION_TYPE from "../../../app/utils/transactionTypeEnum"; import { promises as fs } from "fs"; async function getTransactions() { const file = await fs.readFile( process.cwd() + "/app/lib/transactions/data/mockData.json", "utf-8" ); const transactions = JSON.parse(file); return transactions.slice(0, 3); } const mockTransactionTable = vi.fn(); vi.mock("../../../app/(ui)/components/transactionTable", () => ({ default: (props) => { mockTransactionTable(props); return props.transactions.map((transaction) => transaction.id).join(" "); }, })); describe("async Expenses server component", async () => { const transactions = await getTransactions(); const transactionsIdsString = transactions .map((transaction) => transaction.id) .join(" "); beforeEach(async () => { const Expenses = await ExpensesPage(); render(Expenses); }); afterEach(() => { cleanup(); }); test("renders a header", () => { screen.getByText("Expenses"); }); test("renders a transactions table", () => { expect(mockTransactionTable).toHaveBeenCalledWith( expect.objectContaining({ transactions: transactions, }) ); screen.getByText(transactionsIdsString); }); });
Activity 6 - Mock the
getTransactions
andgetPlanCategories
Server ActionsAt this point, if you save your progress and analyze the results of your tests by running
npm run test
, you will notice that the"renders a transactions table"
test is failing. It will fail because the spy is not being called with what you would expect. The component is accessing database data instead of just using the mock data from the"/app/lib/transactions/data/mockData.json"
file. To correct this, you will need to mock thegetTransactions
andgetPlanCategories
server actions.You will mock the
getTransactions
andgetPlanCategories
server actions in theapp/lib/actions.js
file using Vitest’svi
utility.-
Above the
describe
block, callvi.mock
. -
Pass the relative path to the
TransactionTable
component,”../../../app/lib/actions”
asvi.mock
’s first parameter. -
Pass a lambda function for the second parameter.
-
The lambda function should return an object with the properties
getTransactions
andgetPlanCategories
.- Note: This object that is returned becomes the signature for the mocked module. Both of the methods you are mocking are named exports so the property should be the name of the method.
-
The value of the
getTransactions
property is a lambda function that looks like the code block below:async (transactionType) => { return await getTransactions(); }
-
The value of the
getPlanCategories
is a lambda function that looks like the code block below:
async (planType) => { const transactions = await getTransactions(); return transactions.map((transaction) => { return { id: transaction.planId, name: transaction.plan.name }; }); } ``` <details> <summary>Example <code>ExpensesPage</code> test file</summary> ```js import { expect, test, describe, beforeEach, afterEach, vi } from "vitest"; import { render, screen, cleanup } from "@testing-library/react"; import ExpensesPage from "../../../app/(ui)/expenses/page"; import TRANSACTION_TYPE from "../../../app/utils/transactionTypeEnum"; import { promises as fs } from "fs"; async function getTransactions() { const file = await fs.readFile( process.cwd() + "/app/lib/transactions/data/mockData.json", "utf-8" ); const transactions = JSON.parse(file); return transactions.slice(0, 3); } const mockTransactionTable = vi.fn(); vi.mock("../../../app/(ui)/components/transactionTable", () => ({ default: (props) => { mockTransactionTable(props); return props.transactions.map((transaction) => transaction.id).join(" "); }, })); vi.mock("../../../app/lib/actions", () => ({ getTransactions: async (transactionType) => { return await getTransactions(); }, getPlanCategories: async (planType) => { const transactions = await getTransactions(); return transactions.map((transaction) => { return { id: transaction.planId, name: transaction.plan.name }; }); }, })); describe("async Expenses server component", async () => { const transactions = await getTransactions(); const transactionsIdsString = transactions .map((transaction) => transaction.id) .join(" "); beforeEach(async () => { const Expenses = await ExpensesPage(); render(Expenses); }); afterEach(() => { cleanup(); }); test("renders a header", () => { screen.getByText("Expenses"); }); test("renders a transactions table", () => { expect(mockTransactionTable).toHaveBeenCalledWith( expect.objectContaining({ transactions: transactions, }) ); screen.getByText(transactionsIdsString); }); });
Now when quit any running testing executions by typing
q
in Terminal and you rerun thenpm run test
command to run all of your tests, all tests in the__tests__/(ui)/components/transactionTable.test.jsx
and__tests__/(ui)/expenses/page.test.jsx
files should pass. Your output should look like below: - In the
-
Challenge
Test the `createTransaction` Server Action
Introduction
In this step, you will be implementing the
”createTransaction”
test suite found in the__tests__/lib/actions.test.js
file.Vitest associates this test file with the code found in the
app/lib/actions.js
file.Throughout this step, you will use the utilize all the Vitest methods you have learned about in this lab so far (e.g.
expect
,test
,describe
,beforeEach
,afterEach
, andvi
).The test file you have been given to test server actions already imports the appropriate server actions from the
app/lib/actions.js
file and all other modules you will need to complete this step.
Activity 1 - Mock the
revalidatePath
Method from the External Module“next/cache”
You cannot call
revalidatePath
when testing a Next.js application or you get the following error:Error: Invariant: static generation store missing in revalidatePath “path”
The
createTransaction
server action callsrevalidatePath
on a few different static paths to force a rerender of the associated server components. In order to prevent this error when you write a future test around thecreateTransaction
server action, you will mock”next/caches”
revalidatePath
method.- Above the
"Transaction CRUD Server Actions"
describe
block, callvi.mock
. - Pass the
”next/cache”
module path asvi.mock
’s first parameter. - Pass a lambda function for the second parameter.
- The lambda function should return an object with the property
revalidatePath
. - The value of the
revalidatePath
property is a lambda function that looks like the code block below:
(path) => { return "Path Validated Once More"; },
Example
actions.test.js
import { expect, test, describe, beforeEach, afterEach, vi } from "vitest"; import { promises as fs } from "fs"; import { createTransaction, getTransaction, getTransactions, updateTransaction, deleteTransaction, } from "../../app/lib/actions"; import TRANSACTION_TYPE from "../../app/utils/transactionTypeEnum"; import prisma from "../../app/__mocks__/db"; async function getTestTransactions() { const file = await fs.readFile( process.cwd() + "/app/lib/transactions/data/mockData.json", "utf-8" ); const transactions = JSON.parse(file); return transactions; } vi.mock("next/cache", () => ({ revalidatePath: (path) => { return "Path Validated Once More"; }, })); describe("Transaction CRUD Server Actions", () => { const oneMonthCadence = { id: "1", name: "1-month", }; const expenseType = { id: "1", name: TRANSACTION_TYPE.EXPENSE, }; const mortgagePlan = { id: "1", budgetAmount: 5000, name: "Mortgage", description: "A Place To Live", cadenceId: "1", typeId: "1", }; let testTransactions; beforeEach(async () => { testTransactions = await getTestTransactions(); }); describe("createTransaction", () => { const formDataObject = { date: "10-12-2002", amount: "5000", notes: "", category: "Mortgage", }; const expectedNewTransaction = { date: "2002-10-12T06:00:00.000Z", amount: "5000", notes: "", planId: "1", id: "1", }; const formData = new FormData(); beforeEach(() => { for (var key in formDataObject) { formData.append(key, formDataObject[key]); } }); }); });
Activity 2 - Write a Test to Verify the Return Value from the
createTransaction
Server Action- In the
”createTransaction”
describe
block and under thisdescribe
’sbeforeEach
block, call Vitest’stest
. - Name the test
”returns the new transaction in JSON format”
. - Pass a lambda function for the second parameter.
- Inside the lambda function,
await
the results of callingcreateTransaction
withTRANSACTION_TYPE.EXPENSE
andformData
as parameters respectively. - Store the result of
createTransaction
in a constant variable titledtransaction
. - Assert that
transaction
equals theexpectedNewTransaction
object initialized earlier in”createTransaction”
test suite.- Note: This should be a deep comparison which can be done with the
chai
toStrictEqual
assertion.
- Note: This should be a deep comparison which can be done with the
Example
createTransaction
describe
blockdescribe("createTransaction", () => { const formDataObject = { date: "10-12-2002", amount: "5000", notes: "", category: "Mortgage", }; const expectedNewTransaction = { date: "2002-10-12T06:00:00.000Z", amount: "5000", notes: "", planId: "1", id: "1", }; const formData = new FormData(); beforeEach(() => { for (var key in formDataObject) { formData.append(key, formDataObject[key]); } }); test("returns the new transaction in JSON format", async () => { const transaction = await createTransaction( TRANSACTION_TYPE.EXPENSE, formData ); expect(transaction).toStrictEqual({ ...expectedNewTransaction, }); }); });
Activity 3 - Mock the Prisma Client
In situations like unit testing, which focus on a single function, the best practice is to assume that your database operations will behave correctly and use a mocked version of your client or driver instead. This will allow you to focus on testing the specific behavior of the function you are targeting without dealing with specifics of other functions and modules. This practice aligns with what you may have heard as "separation of concerns."
Note: There are scenarios where you may want to test against a database and actually perform operations on it. Integration and end-to-end tests are good examples of these cases. These tests may rely on multiple database operations occurring across multiple functions and areas of your application.
If you run your tests at this point, you will notice that the result from the
createTransaction
server action is not what you expected. If you look closely, you will notice it is theid
andplanId
that is never what you expect because it is a random guid determined by Prisma.To fix this issue, you will mock the
Prisma Client
so you are not interacting with the database when running tests. This will allow you to establish what ids you expect to receive from thecreateTransaction
server action.It is considered best practice to create a mock of your client to properly unit test your functions that use Prisma Client. This mock will replace the imported module that your function would normally use.
- Above the
"Transaction CRUD Server Actions"
describe
block, callvi.mock
. - Pass
../../app/db
as the module to mock for the first parameter.- Note: This time you will not supply a factory for what to return when this mock is called.
- Note:
app/db.ts
is whereprisma
is defined globally to be an instance of aPrismaClient
. You are mocking this module because you want to override the global instance ofprisma
with a mocked version ofPrismaClient
.
Example mock to add to
actions.test.js
vi.mock("../../app/db");
Activity 4 - Deep Mock the Prisma Client
Currently, Vitest will attempt to mock the module found in the
app/db.ts
file, however it will not be able to automatically mock the "deep" or "nested" properties of theprisma
object. For example,prisma.transaction.create()
will not be mocked properly as it is a deeply nested property of thePrisma Client
instance. This causes the tests to fail as the function will still run as it normally does against the real database.To solve this problem, you need to let Vitest know how exactly you want that module to be mocked and provide it the value that should be returned when the mocked module is imported, which should include mocked versions of the deeply nested properties.
Inside the
app/__mocks__/db.ts
file, do the following:- Call Vitest’s
beforeEach
method to reset theprisma
mock before each test is ran.- Note: This is done by calling the
”vitest-mock-extended”
mockReset
method and passingprisma
as a parameter.
- Note: This is done by calling the
- Assign the constant variable
prisma
tomockDeep<PrismaClient>();
instead of a new instance ofPrismaClient
.- Note: Essentially,
mockDeep
will set everyPrisma Client
function's value tovi.fn()
.
- Note: Essentially,
The folder name
__mocks__
is a common convention in testing frameworks where you may place any manually created mocks of modules. The__mocks__
folder must be directly adjacent to the module you are mocking.Example
app/__mocks__/db.ts
file contentsimport { PrismaClient } from "@prisma/client"; import { beforeEach } from "vitest"; import { mockDeep, mockReset } from "vitest-mock-extended"; beforeEach(() => { mockReset(prisma); }); const prisma = mockDeep<PrismaClient>(); export default prisma;
Before running your tests again, import the mocked
Prisma Client
variableprisma
from theapp/__mocks__/db.ts
file in the__tests__/lib/actions.test.js
file.Example mocked prisma import
import prisma from "../../app/__mocks__/db";
Activity 5 - Mock Prisma Client Methods
If you run your tests now, you will now see the following type error for the
”returns the new transaction in JSON format”
test:TypeError: Cannot read properties of undefined (reading 'id')
This error actually occurs because the
prisma
mock has been put in place correctly. Yourprisma.type.findUnique
invocation in thegetTypeByName
server action in theapp/lib/actions.js
file is no longer hitting the database. Currently, that function essentially does nothing and returnsundefined
. Then thegetPlanByTypeAndPlanName
server action tries to accessid
onundefined
which is aTypeError
.You need to tell Vitest what
prisma.type.findUnique
,prisma.plan.findUnique
, andprisma.transaction.create
should do by mocking its behavior. Now that you have a proper mocked version of the Prisma Client, you need to do a simple change to your test’s before action.- Add the following code to your
beforeEach
lambda function inside the”createTransaction”
describe
block:
prisma.transaction.create.mockResolvedValue({ ...expectedNewTransaction, }); prisma.type.findUnique.mockResolvedValue({ ...expenseType, }); prisma.plan.findUnique.mockResolvedValue({ ...mortgagePlan, });
Example
actions.test.js
file contentsimport { expect, test, describe, beforeEach, afterEach, vi } from "vitest"; import { promises as fs } from "fs"; import { createTransaction, getTransaction, getTransactions, updateTransaction, deleteTransaction, } from "../../app/lib/actions"; import TRANSACTION_TYPE from "../../app/utils/transactionTypeEnum"; import prisma from "../../app/__mocks__/db"; async function getTestTransactions() { const file = await fs.readFile( process.cwd() + "/app/lib/transactions/data/mockData.json", "utf-8" ); const transactions = JSON.parse(file); return transactions; } vi.mock("next/cache", () => ({ revalidatePath: (path) => { return "Path Validated Once More"; }, })); vi.mock("../../app/db"); describe("Transaction CRUD Server Actions", () => { const oneMonthCadence = { id: "1", name: "1-month", }; const expenseType = { id: "1", name: TRANSACTION_TYPE.EXPENSE, }; const mortgagePlan = { id: "1", budgetAmount: 5000, name: "Mortgage", description: "A Place To Live", cadenceId: "1", typeId: "1", }; let testTransactions; beforeEach(async () => { testTransactions = await getTestTransactions(); }); describe("createTransaction", () => { const formDataObject = { date: "10-12-2002", amount: "5000", notes: "", category: "Mortgage", }; const expectedNewTransaction = { date: "2002-10-12T06:00:00.000Z", amount: "5000", notes: "", planId: "1", id: "1", }; const formData = new FormData(); beforeEach(() => { for (var key in formDataObject) { formData.append(key, formDataObject[key]); } prisma.transaction.create.mockResolvedValue({ ...expectedNewTransaction, }); prisma.type.findUnique.mockResolvedValue({ ...expenseType, }); prisma.plan.findUnique.mockResolvedValue({ ...mortgagePlan, }); }); test("returns the new transaction in JSON format", async () => { const transaction = await createTransaction( TRANSACTION_TYPE.EXPENSE, formData ); expect(transaction).toStrictEqual({ ...expectedNewTransaction, }); }); }); });
After this, all three test files should pass and all four test methods implemented in this lab should pass. If you quit any running test executions in Terminal by typing
q
and rerunning tests with the commandnpm run test
, your output should look like the picture below. You can ignore any warnings and only focus on what comes after them. - Above the
-
Challenge
Conclusion
Conclusion
In this lab, you became familar with how Vitest is used to test an existing Next.js application. You tested a Next.js client component, server component, and server action using Vitest. You can see how you were able to test backend and frontend code using Vitest. In addition, you learned about the shortcomings of Vitest when it comes to testing server components. You mocked internal and external modules to complete tests and maintained separation of concerns. The finance application in the lab is well on its way to being well tested at this point, which is a huge accomplishment!
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.