- Lab
- Core Tech

Guided: Unit Testing in JavaScript
This code lab will teach you how to implement unit tests for JavaScript/Node.js applications using Jest. You'll learn essential concepts and techniques, including test structure, assertions, mocking, asynchronous testing, and test coverage.

Path Info
Table of Contents
-
Challenge
Introduction
Welcome to the lab Guided: Unit Testing in JavaScript.
In this code lab, you will implement unit tests using Jest for a shopping cart application built with Node.js and Express.
The application has the following functionality:
- Display a catalog of products
- Allow users to add products to the shopping cart
- Display the contents of the shopping cart
- Allow users to update the quantity of items in the cart
- Allow users to remove items from the cart
- Clear the entire cart
- Simulate the checkout process with an asynchronous operation and clear the cart upon completion
To run the application, either click the Run in the bottom-right corner of the screen or in a Terminal tab execute:
npm start
Then, click the following link to open the application in a new browser tab: {{localhost:3000}}. Alternatively, you can refresh the Web Browser tab next to the Terminal tabs to reload and view the application there.
Note that an additional Terminal tab has been provided to you, so you can run the other commands you'll be executing throughout this lab. ---
Familiarizing with the Program Structure
The application includes the following files:
-
app.js: The main entry point of the application, which sets up the Express server and defines routes.
-
controllers/cartController.js: Contains the controller functions for handling cart-related operations.
-
models/cart.js: Defines the
Cart
class that manages the shopping cart functionality. -
routes/appRoutes.js: Defines the routes for the shopping cart application.
-
data/products.js: Contains the sample product data.
-
views/index.ejs: The EJS template for rendering the shopping cart page.
-
views/products.ejs: The EJS template for rendering the product catalog page.
However, you'll focus on configuring Jest and writing test files for different parts of the application.
Start by examining the provided code and understanding the program structure. When you're ready, dive into the coding process. If you encounter any problems, remember that a
solution
directory is available for reference or to verify your code. -
Challenge
Step 1: Setting Up Jest
Setup and Configuration
To start using Jest, you first need to add it to your project's dependencies. You can do this by running the following command in your project's root directory:
npm install --save-dev jest
This command installs Jest as a development dependency (
--save-dev
) in your project. However, in this environment, Jest is already installed, so you don't have to execute the above command.Once Jest is installed, you can run your tests using the
jest
command in your terminal. By default, Jest looks for test files in a__tests__
directory, or in files with a.spec.js
,.spec.jsx
,.test.js
, or.test.jsx
extension.For example, if you have a test file named
myFunction.test.js
, you can run it with Jest by executing:npx jest myFunction.test.js
npx
is a tool that comes withnpm
(Node Package Manager). It is used to execute binaries fromnode_modules
without having to install them globally. So, when you run the above command,npx
looks for thejest
binary in your project'snode_modules/.bin
directory and executes it, allowing you to run your tests without requiring a global installation of Jest.However, it's often more convenient to set up a test script in your project's
package.json
file. This allows you to run your tests with a simple command likenpm test
. This offers several benefits:- Consistency: All developers working on the project can run tests the same way, regardless of their environment.
- Simplicity: Running
npm test
is easier and less error-prone than remembering and typing out the full Jest command. - Customization: You can add additional options or flags to your test script to customize Jest's behavior for your project.
The
node_modules
directory of this lab already contains the Jest dependency. In the next task, you'll set up the test script. After completing the task, you'll be able to run your Jest tests by executing the commandnpm test
in the terminal. However, keep in mind that since we're just beginning, only a few tests will pass at this stage. ---The
jest.config.js
FileBy default, Jest works well out of the box, but sometimes you may need to tweak its settings to better suit your project's needs. That's where the
jest.config.js
file comes in handy.The
jest.config.js
file is a special file that allows you to customize Jest's configuration for your project. It is automatically detected by Jest when running your tests.Here are some of the most important options you can configure in the
jest.config.js
file:-
verbose
: A boolean that indicates whether Jest should output verbose information about the test run. Setting it totrue
can be helpful for debugging. -
testMatch
: An array of glob patterns that Jest uses to detect test files. Remember, by default, Jest looks for files with.test.js
,.spec.js
, or.js
extensions inside__tests__
folders. -
setupFiles
: An array of paths to modules that run some code to configure or set up the testing environment before each test file in the suite is executed. -
collectCoverage
: A boolean that indicates whether Jest should collect code coverage information during the test run. -
moduleNameMapper
: An object that maps module names to their corresponding paths, allowing you to stub out resources like images or styles with a single module.
Here's an example of what a
jest.config.js
file might look like:module.exports = { verbose: true, testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'], setupFiles: ['./setupTests.js'], collectCoverage: true, moduleNameMapper: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js', '\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js', }, };
In the next task, you will configure Jest to output verbose information.
-
Challenge
Step 2: Writing Tests
Test Structure: Given-When-Then
A common pattern for structuring tests is the Given-When-Then approach:
- Given: Set up the necessary preconditions and inputs for the test.
- When: Perform the action or behavior that you want to test.
- Then: Assert the expected outcomes or results.
This structure helps make your tests clear, readable, and focused on a single behavior or scenario.
In Jest, you can use the
test
function to define individual test cases. Thetest
function takes two arguments:- A string description of the test case.
- A function that contains the test code.
Here's an example:
test('should add two numbers', () => { // Given const a = 2; const b = 3; // When const sum = a + b; // Then expect(sum).toBe(5); });
In this example, the test verifies that adding two numbers produces the expected result. The
expect
function is used to make assertions about the test outcomes.Also, when labeling your test functions, it's a common convention to use the word should followed by a description of the expected behavior, as in the above example:
test('should add two numbers', () => { // Test logic... });
Using should in the test label helps to express the intended behavior or outcome of the test in a clear and readable way. It communicates the expectation of what the code being tested should do.
While this convention is not strictly enforced by Jest, it is widely adopted and considered a good practice for writing clear and maintainable tests.
In the following tasks, you'll write a test using Jest's
test
function. After completing this task, you will have added an emptytest
function to verify the functionality of adding an item to the cart. In the next task, you will implement the test logic inside this function. The test is now complete. It creates a newCart
instance, defines an item object, adds the item to the cart using theaddItem
method, and asserts that theitem
object is present in thecart.items
array.You can run the test by executing the following command:
npm test __tests__/addItemCart.test.js ``` --- ### Grouping Tests with `describe` When you have multiple related test cases, you can group them together using the `describe` function. The `describe` function takes two arguments: 1. A string description of the group of tests. 2. A function that contains the related test cases. Here's an example: ```javascript describe('Math operations', () => { test('should add two numbers', () => { // ... }); test('should subtract two numbers', () => { // ... }); // More related test cases... });
Grouping tests with
describe
helps organize your test suite and makes it easier to understand the structure of your tests.Jest also provides helper functions (also called hooks) to handle setup and teardown logic for your tests:
beforeEach
: Runs before each test case in the current test suite.beforeAll
: Runs once before all the test cases in the current test suite.afterEach
: Runs after each test case in the current test suite.afterAll
: Runs once after all the test cases in the current test suite.
These functions allow you to set up necessary preconditions, clean up resources, or perform any other required actions before or after your tests run.
In the upcoming tasks, you'll practice using Jest's
describe
function and explore how to usebeforeEach
to extract common setup logic. By wrapping the test functions inside adescribe
block, you group related test cases together, making the test suite more organized and readable. However, there's common code in the test methods that can be extracted into abeforeEach
method. You will do this in the next task. By extracting the cart creation logic into abeforeEach
hook, you ensure that a newCart
instance is created before each test case. This promotes test isolation and helps maintain a clean state for each test.However, you might be wondering about the creation of the
item
object within each test case. The recommendation is to extract only theCart
instance creation into thebeforeEach
hook and keeping theitem
objects within each test case. Here's the reasoning:-
The
Cart
instance is created in the same way across all the test cases, so extracting it into thebeforeEach
hook reduces duplication and ensures a freshCart
instance for each test case. -
The
item
objects used in the test cases have different properties and values. For example, in the first test case,item
has aquantity
of 1, while in the second test case,item1
anditem2
have differentquantity
values. Extracting theitem
objects into a shared variable may lead to confusion and make the test cases less readable. -
Keeping the
item
objects within each test case makes the tests more self-contained and independent. It allows for easier modification of theitem
objects specific to each test case without affecting others.
You can run the tests by executing the following command:
npm test __tests__/updateQuantityCart.test.js
-
Challenge
Step 3: Using Matchers
What are Matchers?
Matchers are methods provided by Jest that let you compare and verify values in your tests. They are used in conjunction with the
expect
function to create assertions.In the previous lesson, we already used a matcher in the
addItemCart.test.js
file:expect(cart.items).toContainEqual(item);
Here,
toContainEqual
is a matcher that checks if thecart.items
array contains an element that matches theitem
object.Jest provides a wide range of matchers to cover different assertion scenarios. Let's take a look at some commonly used matchers.
Common Matchers
-
toBe
: Checks strict equality (===) between the actual and expected values.expect(result).toBe(42);
-
toEqual
: Checks deep equality between the actual and expected values, recursively comparing all properties of objects and arrays.expect(obj).toEqual({ name: 'John', age: 30 });
-
toContain
: Checks if an array or string contains a specific element or substring.expect(array).toContain('apple'); expect(string).toContain('hello');
-
toBeDefined
: Checks if a value is not undefined.expect(result).toBeDefined();
-
toBeTruthy
: Checks if a value is truthy (evaluates to true in a boolean context).expect(result).toBeTruthy();
-
toBeFalsy
: Checks if a value is falsy (evaluates to false in a boolean context).expect(result).toBeFalsy();
-
toBeGreaterThan
,toBeGreaterThanOrEqual
,toBeLessThan
,toBeLessThanOrEqual
: Checks if a value is greater than, greater than or equal to, less than, or less than or equal to another value.expect(result).toBeGreaterThan(10); expect(result).toBeLessThanOrEqual(5);
-
toMatch
: Checks if a string matches a regular expression.expect(string).toMatch(/hello/);
-
toThrow
: Checks if a function throws an error when called.expect(() => { throw new Error('An error occurred'); }).toThrow();
These are just a few examples of the matchers available in Jest. You can find the complete list of matchers in the Jest documentation.
Negating Matchers
You can invert the behavior of any matcher by chaining the
.not
modifier before the matcher:expect(value).not.toBe(42); expect(array).not.toContain('banana');
This is useful when you want to assert that something should not be true or should not have a certain value.
In the next tasks, you'll practice using matchers to add assertions to existing tests in the
__tests__/otherFunctionalityCart.test.js
file. The test file complete. You can run it by executing the following command:npm test __tests__/otherFunctionalityCart.test.js
-
-
Challenge
Step 4: Using Mocks
What is Mocking?
Mocking is a technique used in testing to replace real objects or functions with simulated versions that mimic their behavior. Mocks allow you to control the behavior of dependencies, isolate the code being tested, and verify interactions between different parts of your codebase.
Jest provides built-in mocking capabilities through the
jest.mock
andjest.fn
functions.Using jest.mock
The
jest.mock
function is used to automatically mock a module or a function. When you usejest.mock
, Jest replaces the actual implementation of the module or function with a mocked version.Here's an example of using
jest.mock
to mock a module:jest.mock('../src/models/product');
In this case, Jest will replace the actual implementation of the
product
module with a mocked version. Any functions exported by theproduct
module will be replaced with mock functions.Using jest.fn
The
jest.fn
function is used to create a standalone mock function. It allows you to create a new function that can be tracked and configured with custom behavior.Here's an example of using
jest.fn
to create a mock function:const mockFunction = jest.fn();
You can then use the mock function in your code and assert its behavior.
Configuring Mock Behavior
Once you have created a mock using
jest.mock
orjest.fn
, you can configure its behavior using methods likemockReturnValue
ormockResolvedValue
.For example, to configure a mock function to return a specific value:
mockFunction.mockReturnValue(42);
Now, whenever the
mockFunction
is called, it will return the value42
.Asserting Mock Interactions
Jest provides assertion functions to verify the interactions with mocks. One commonly used assertion is
toHaveBeenCalledWith
, which checks if a mock function was called with specific arguments.Here's an example of using
toHaveBeenCalledWith
:expect(mockFunction).toHaveBeenCalledWith('arg1', 'arg2');
This assertion verifies that
mockFunction
was called with the arguments'arg1'
and'arg2'
.Other useful assertions for mocks include:
toHaveBeenCalled
: Checks if a mock function was called at least once.toHaveBeenCalledTimes
: Verifies the number of times a mock function was called.toHaveBeenLastCalledWith
: Checks the arguments of the last call to a mock function.
By using these assertions, you can ensure that your mocks are being invoked correctly and with the expected arguments.
In the next tasks, you'll practice configuring mocks using
jest.mock
andjest.fn
, and you'll use assertions liketoHaveBeenCalledWith
to verify mock interactions in thecartController.test.js
file. Once you have added the necessary code to mock theCart
model and products data, the tests will use the mocked versions instead of the actual implementations.However, since the tests are testing the controller, you also need to mock the response objects. You'll do it in the next task. After replacing the
null
values with appropriate mock functions usingjest.fn()
, theres
object will have mocked versions of therender
,redirect
,json
, andstatus
methods available for use in your tests. This setup ensures fresh request and response objects before each test.Additionally, the
afterEach
hook will clear all mocks after each test:afterEach(() => { jest.clearAllMocks(); });
The only remaining task is to practice using the
toHaveBeenCalledWith
matcher. To keep it simple, you'll complete the implementation of the first test case only. Once you have added theexpect
statements with the appropriatetoHaveBeenCalledWith
matchers, the test case will verify that theremoveItem
function is called with the correct item ID and that theredirect
function is called with the correct URL.You can see other examples of how to use the
toHaveBeenCalledWith
matcher in the othertest
functions.Finally, you can run the test file by executing the following command:
npm test __tests__/cartController.test.js
-
Challenge
Step 5: Asynchronous Testing
Testing Promises
When testing asynchronous code that uses promises, you can use the
resolves
andrejects
matchers in combination withawait
to make assertions about the resolution or rejection of a promise.Here's an example of testing a promise that resolves successfully:
test('should fetch user data successfully', async () => { const userData = { id: 1, name: 'John' }; const fetchUser = () => Promise.resolve(userData); await expect(fetchUser()).resolves.toEqual(userData); });
In this test, we have a function
fetchUser
that returns a promise resolving touserData
. We useawait
to wait for the promise to resolve and theresolves
matcher to assert that the promise resolves with the expecteduserData
.Similarly, you can test a promise that rejects with an error:
test('should throw an error when fetching user data fails', async () => { const errorMessage = 'Failed to fetch user data'; const fetchUser = () => Promise.reject(new Error(errorMessage)); await expect(fetchUser()).rejects.toThrow(errorMessage); });
Here, we have a function
fetchUser
that returns a promise rejecting with an error. We use therejects
matcher along withtoThrow
to assert that the promise rejects with the expected error message.In the next tasks, you'll complete the implementation of the unit tests for the asynchronous checkout functionality, covering both resolved and rejected promise scenarios. Once you have added the
expect
statements with the appropriate matchers, the test cases will verify that thecheckout
method resolves or rejects with the expected success or error messages.You can run the tests by executing the following command:
npm test __tests__/checkoutCart.test.js ``` --- ### Mocking Asynchronous Behavior When testing code that depends on asynchronous behavior, you may need to mock the asynchronous functions to control their behavior in tests. Jest provides methods like `mockResolvedValue` and `mockRejectedValue` to mock the resolution or rejection of a promise. Here's an example of mocking a promise to resolve with a specific value: ```javascript test('should display user data when API call succeeds', async () => { const userData = { id: 1, name: 'John' }; api.getUserData.mockResolvedValue(userData); // Test code that calls getUserData and displays user data expect(screen.getByText('John')).toBeInTheDocument(); });
In this test, we mock the
getUserData
method of anapi
module usingmockResolvedValue
to resolve withuserData
. The test can then assert that the user data is displayed correctly.Similarly, you can mock a promise to reject with an error:
test('should display error message when API call fails', async () => { const errorMessage = 'Failed to fetch user data'; api.getUserData.mockRejectedValue(new Error(errorMessage)); // Test code that calls getUserData and displays error message expect(screen.getByText(errorMessage)).toBeInTheDocument(); });
Here, we use
mockRejectedValue
to mock thegetUserData
method to reject with the specified error. The test can then assert that the error message is displayed appropriately.In the next tasks, you'll complete the implementation of the unit tests for the asynchronous checkout functionality, mocking both resolved and rejected values to test the checkout controller. Once you have added the lines to mock the resolved and rejected values of the
checkout
method, the test cases will use the mocked values instead of the actual implementation when callingcartController.checkout
.You can run the tests by executing the following command:
npm test __tests__/checkoutCartController.test.js
-
Challenge
Step 6: Test Coverage
What is Test Coverage?
Test coverage is a metric that measures the extent to which your code is covered by tests. It helps you identify parts of your codebase that lack sufficient testing and ensures that your tests are comprehensive.
Jest provides built-in support for generating test coverage reports. These reports show you the percentage of code that is executed when running your tests, highlighting areas that are not covered.
Configuring Test Coverage
To enable test coverage in Jest, you need to configure it in the
jest.config.js
file. Here are the key options related to test coverage:-
collectCoverage
: Set this option totrue
to enable test coverage collection.module.exports = { collectCoverage: true, // ... };
-
collectCoverageFrom
: Specify the files and patterns for which coverage information should be collected. You can use glob patterns to match specific files or directories.module.exports = { // ... collectCoverageFrom: ["src/dir1/*.{js,jsx}", "src/dir2/*.{js,jsx}", "src/dir3/*.{js,jsx}"], // ... };
In this example, coverage will be collected for all
.js
and.jsx
files in thesrc/dir1
,src/dir2
, andsrc/dir3
directories. -
coverageThreshold
: Set minimum coverage thresholds for statements, branches, functions, and lines. If the coverage falls below the specified thresholds, Jest will fail the tests.module.exports = { // ... coverageThreshold: { global: { statements: 80, branches: 80, functions: 80, lines: 80, }, }, // ... };
In this example, the global coverage thresholds are set to 80% for statements, branches, functions, and lines. Jest will fail the tests if the coverage falls below these thresholds.
Analyzing Coverage Reports
When you run your tests with coverage enabled, a report is generated automatically. This report provides detailed information about the coverage of your code. It includes:
- Coverage percentages for statements, branches, functions, and lines.
- A list of files and their individual coverage percentages.
- Highlighted code showing covered and uncovered lines.
You can open the
coverage/lcov-report/index.html
file in a web browser to view an interactive coverage report.Achieving 100% Test Coverage
While striving for 100% test coverage is a good goal, it's important to note that it doesn't guarantee bug-free code. However, high test coverage increases confidence in your codebase and helps catch potential issues.
To achieve 100% test coverage, you need to ensure that all statements, branches, functions, and lines of your code are executed by your tests. This may involve adding more test cases, covering edge cases, and testing error scenarios.
In the next tasks, you'll configuring test coverage in the
jest.config.js
file and enable a previously disabled test to achieve 100% coverage. After configuring test coverage in thejest.config.js
file, if you run the all the tests with:npm test
You'll notice two things:
- The coverage threshold for branches (90%) is not met.
- The
coverage
directory with the report is generated.
To achieve the desired coverage, you'll need to enable a currently disabled test in the
updateQuantityCart.test.js
file. By removing.skip
, the test case will now be included in the test execution, contributing to the overall test coverage. If you runnpm test
, you'll see that the tests now achieve 100% coverage. -
-
Challenge
Conclusion
Congratulations on successfully completing this Code Lab!
You've learned the fundamentals of writing unit tests, including:
- Setting up Jest in your project
- Writing test cases using Jest's
test
anddescribe
functions - Using matchers to make assertions about the expected behavior
- Mocking dependencies and asynchronous code
- Configuring and achieving high test coverage
Remember that to run the application, either click the Run in the bottom-right corner of the screen or in a Terminal tab execute:
npm start
Then, click the following link to open the application in a new browser tab: {{localhost:3000}}. Alternatively, you can refresh the Web Browser tab next to the Terminal tabs to reload and view the application there.
To run all the tests, you can use the command
npm test
, which is equivalent tonpx jest
. However, you can also run individual test files by passing the name of the test file as an argument to thenpm test
command. For example,npm test __tests__/addItemCart.test.js
will run only the tests in theaddItemCart.test.js
file.Writing unit tests is an essential practice in software development. It helps ensure the correctness of your application, prevents regression errors, and provides documentation for the expected behavior of your functions and classes. ---
Extending the Program
Consider exploring these ideas to further enhance your skills and expand the capabilities of the program:
-
Implement a wishlist feature. Add functionality to allow users to create and manage a wishlist of products. Write unit tests to verify the behavior of adding items to the wishlist, removing items from the wishlist, and retrieving the wishlist.
-
Implement a product review system. Allow users to leave reviews and ratings for products. Write unit tests to validate the submission of reviews, the calculation of average ratings, and the retrieval of product reviews.
-
Add support for product variations. Extend the product model to include variations such as size, color, or other attributes. Write unit tests to ensure that the variations are properly handled during product selection, cart management, and order processing.
You can follow the TDD (Test-Driven Development) approach when implementing these features. Write the unit tests first, describing the expected behavior, and then implement the functionality to make the tests pass. This will help you maintain high test coverage and ensure that the new features are thoroughly tested. ---
Related Courses on Pluralsight's Library
If you're interested in further honing your JavaScript skills or exploring more topics, Pluralsight offers several excellent courses in the following path:
These courses cover many aspects of JavaScript programming. Check them out to continue your learning journey in JavaScript!
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.