Unit testing with Jest

In this blog, we have picked a library from the JavaScript testing framework, Jest to explain how to do unit testing with some interesting examples. We have utilized some of the key features of Jest library.

GraphQL has a role beyond API Query Language- being the backbone of application Integration
background Coditation

Unit testing with Jest

We previously explained the purpose of testing and how it can help improve the quality of your code along with explaining the fundamental ways to test out JavaScript apps. In the series, we have picked one library to explain how we can do Unit Testing with some interesting examples, utilizing key features of Jest Library.

What is Unit Testing?

"Unit testing is a way to validate that individual units of code are working correctly. By writing unit tests, we can ensure that our code is reliable and free of bugs. This can save us time in the long run, because it's easier to catch and fix problems early on in the development process.”

Let's get started with unit testing using Jest:

We will need to install the package and create a configuration file. Here are the steps to follow to set everything up:

1. Install Jest by running npm install --save-dev jest  or yarn add --dev jest. This will add Jest to your project as a development dependency.
2. npm install jest --global To run jest on command line
3. Create a configuration file by running jest --init. This will prompt you to answer a few questions about your project, and then generate a jest.config.js  file in the root directory of your project.
4. Open the jest.config.js file and modify it as needed. You can specify options such as the test environment, test file patterns, and test transforms.

Here is a detailed info about jest.config.js file:


/*
 * For a detailed explanation regarding each configuration property, visit:
 * https://jestjs.io/docs/configuration
 */

module.exports = {
 // All imported modules in your tests should be mocked automatically
 // automock: false,

  // Stop running tests after n failures
 // bail: 0,

 // The directory where Jest should store its cached dependency information
 // cacheDirectory: “/tmp/jest_rs”,

 // Automatically clear mock calls, instances, contexts and results before every test
 clearMocks: true,

 // Indicates whether the coverage information should be collected while executing the test
 // collectCoverage: false,

 // An array of glob patterns indicating a set of files for which coverage information should be collected
 // collectCoverageFrom: undefined,

 // The directory where Jest should output its coverage files
 // coverageDirectory: undefined,

 // An array of regexp pattern strings used to skip coverage collection
 // coveragePathIgnorePatterns: [
 //  “/node_modules/”
 // ],

 // Indicates which provider should be used to instrument code for coverage
 // coverageProvider: “babel”,

 // A list of reporter names that Jest uses when writing coverage reports
 // coverageReporters: [
 //  “json”,
 //  “text”,
 //  “lcov”,
 //  “clover”
 // ],

 // An object that configures minimum threshold enforcement for coverage results
 // coverageThreshold: undefined,

 // A path to a custom dependency extractor
 // dependencyExtractor: undefined,

 // Make calling deprecated APIs throw helpful error messages
 // errorOnDeprecated: false,

	// The default configuration for fake timers,to mock the timout or 
	//timeinterval like apis
  fakeTimers: {
   “enableGlobally”: false
  },

 // Force coverage collection from ignored files using an array of glob patterns
 // forceCoverageMatch: [],

 // A path to a module which exports an async function that is triggered once before all test suites
 // globalSetup: undefined,

 // A path to a module which exports an async function that is triggered once after all test suites
 // globalTeardown: undefined,

 // A set of global variables that need to be available in all test environments
  globals: {},

 // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
 // maxWorkers: “50%“,

 // An array of directory names to be searched recursively up from the requiring module’s location
 // moduleDirectories: [
 //  “node_modules”
 // ],

 // An array of file extensions your modules use
  moduleFileExtensions: [
   “js”,
   “mjs”,
   “cjs”,
   “jsx”,
   “ts”,
   “tsx”,
   “json”,
   “node”
  ],

 // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
 // moduleNameMapper: {},

 // An array of regexp pattern strings, matched against all module paths before considered ‘visible’ to the module loader
 // modulePathIgnorePatterns: [],

 // Activates notifications for test results
 // notify: false,

 // An enum that specifies notification mode. Requires { notify: true }
 // notifyMode: “failure-change”,

 // A preset that is used as a base for Jest’s configuration
 // preset: undefined,

 // Run tests from one or more projects
 // projects: undefined,

 // Use this configuration option to add custom reporters to Jest
 // reporters: undefined,

 // Automatically reset mock state before every test
 // resetMocks: false,

 // Reset the module registry before running each individual test
 // resetModules: false,

 // A path to a custom resolver
 // resolver: undefined,

 // Automatically restore mock state and implementation before every test
 // restoreMocks: false,

 // The root directory that Jest should scan for tests and modules within
 // rootDir: undefined,

 // A list of paths to directories that Jest should use to search for files in
 // roots: [
 //  “”
 // ],

 // Allows you to use a custom runner instead of Jest’s default test runner
 // runner: “jest-runner”,

 // The paths to modules that run some code to configure or set up the testing environment before each test
 // setupFiles: [],

 // A list of paths to modules that run some code to configure or set up the testing framework before each test
 // setupFilesAfterEnv: [],

 // The number of seconds after which a test is considered as slow and reported as such in the results.
 // slowTestThreshold: 5,

 // A list of paths to snapshot serializer modules Jest should use for snapshot testing
 // snapshotSerializers: [],

 // The test environment that will be used for testing
 // testEnvironment: “jest-environment-node”,

 // Options that will be passed to the testEnvironment
 // testEnvironmentOptions: {},

 // Adds a location field to test results
 // testLocationInResults: false,

 // The glob patterns Jest uses to detect test files
 testMatch: [
    “*/__tests__/*/*.[jt]s?(x)“,
    “*/?(.)+(spec|test).[tj]s?(x)”
 ],

 // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
 testPathIgnorePatterns: [
  “/node_modules/”
 ],

 // The regexp pattern or array of patterns that Jest uses to detect test files
 // testRegex: [],

 // This option allows the use of a custom results processor
 // testResultsProcessor: undefined,

 // This option allows use of a custom test runner
 // testRunner: “jest-circus/runner”,

 // A map from regular expressions to paths to transformers
 // transform: undefined,

 // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
  transformIgnorePatterns: [
   “/node_modules/“,
   “\\.pnp\\.[^\\/]+$”
  ],

 // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
 // unmockedModulePathPatterns: undefined,

 // Indicates whether each individual test should be reported during the run
 // verbose: undefined,

 // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
 // watchPathIgnorePatterns: [],

 // Whether to use watchman for file crawling
 // watchman: true,
};

We can customize the configuration file to suit our needs. For example, one can specify additional options such as the test environment, test reporters, and test coverage thresholds.

5. Create a package.json file. This is where one can define scripts for running your tests.
6. In the package.json file, add a script for running the tests. For example:


"scripts": {    "test": "jest"  }

This script allows us to run our tests by typing npm test or yarn test on the command line.

  1. Create a directory for the test files. By default, Jest looks for test files in a directory called __tests__, It can also, be configured to look in a different location as per preference.
  2. Write the test files and place them in the test directory. Test files should have a .test.js or .spec.js extension.
  3. Run the tests by typing npm test or yarn test on the command line.

Now we have the basic setup of jest & we are ready to test our app.

Simple unit test with Jest
  • Here's an example of a function that adds two numbers together, along with a corresponding test:

// add.js

function add(a, b) {
  return a + b;
}

module.exports = add;

// add.test.js

const add = require('./add');

test('adds 1 + 2 to equal 3', () => {
  expect(add(1, 2)).toBe(3);
});

Let's understand what is happening here:

  • The first line test ('adds 1 + 2 to equal 3', () => { defines a test case with the title "adds 1 + 2 to equal 3".
  • The second line expect (add(1, 2)).toBe(3); is an assertion that checks the result of calling the add function with the arguments 1 and 2.
  • The toBe matcher is used to check that the result of the add function is equal to 3.

This test case is testing the add function, which is expected to take two numbers as arguments and return their sum. The test case verifies that the add function produces the correct result when given the input 1 and 2.

In simple words this example is equivalent to this:


const add = (a, b) => a - b;

const result = add(1, 2);
const expected = 3;
if (result !== expected) {
  throw new Error(`${result} is not equal to ${expected}`);
}

Just the difference is libraries have the all boiler code written for us so that we can directly use this as functions and avoid writing the same code again and again.

  • Let’s take another example where we are getting some data from API and we want to test whether that data is similar to the one which we are expecting. We can achieve this using jest's built-in mocking functionality.

What is Mocking?

Mocking is just like mimicking the behavior functions that you don’t want to be computed. This can be useful for testing code that depends on external resources such as APIs or database queries because it allows you to test the code without actually making the external calls.
Let’s understand this with an example.
Say we have to write the unit test case to check whether there is debit transaction from a bank account, a bank account module will look like this


// bankAccount.js

const database = require('./database');

async function getAccount(id) {
  return database.query(`SELECT * FROM accounts WHERE id = ${id}`);
}

async function debitTransaction(id, amount) {
  const account = await getAccount(id);
  if (account.balance < amount) {
    return false;
  }
  account.balance -= amount;
  await database.query(`UPDATE accounts SET balance = ${account.balance} WHERE id = ${id}`);
  return true;
}

module.exports = {
  getAccount,
  debitTransaction
};

By looking at this file you can understand i have one method - getAccount ,  to get account details for a particular id from database which gives me account balance in the response


// getAccountResult
{
  id: 1,
  balance: 100
}

And the second method is debitTransaction which results in debiting the corresponding amount ( Specified in the argument ) from the specified bank account id ( Specified in the argument ) and returning True if successfully debited the amount and return false if there is no sufficient balance in the account.
To avoid making these database queries while testing our application, we can Mock these methods to return the same data without actually computing these methods, and you can do like this in jest and test the functionality.



const bankAccount = require("../bankAccount");

// Test to Fetch Current Account Balance 
test("Fetch Account Balance", () => {
  // Arrange
  bankAccount.getAccount = jest.fn((id) => ({
    id: id,
    balance: 100,
  }));

  // Act
  const getAccountResult = bankAccount.getAccount(1);

  // Assert
  expect(getAccountResult.id).toBe(1);
  expect(getAccountResult.balance).toBe(100);
});


In this test case we are mocking ( Overriding ) the getAccountfunction from the bankAccount module to return an object with the correct id and balance properties based on the input arguments.



const bankAccount = require("../bankAccount");

// Test to Make a Debit Transaction on account with current balance 100

const current_balance_in_account=100
test("debitTransaction", () => {
  // Arrange
  bankAccount.debitTransaction = jest.fn((id, amount) => {
    if (amount > current_balance_in_account) {
      return false;
    } else {
      return true;
    }
  });

  // Act
  const result = bankAccount.debitTransaction(1, 50);

  bankAccount.getAccount = jest.fn((id) => ({
    id: id,
    balance: 50,
  }));

  const getAccountResult = bankAccount.getAccount(1);

  // Assert
  expect(result).toBe(true);
  expect(bankAccount.getAccount).toHaveBeenCalledWith(1);
  expect(bankAccount.debitTransaction).toHaveBeenCalledWith(1, 50);
  expect(getAccountResult.balance).toBe(50);
});



// Test to debit amount from bank with insufficient funds 
test("debitTransaction with insufficient funds", () => {
  // Arrange
  bankAccount.debitTransaction = jest.fn((id, amount) => {
    if (amount > current_balance_in_account) {
      return false;
    } else {
      return true;
    }
  });

  // Act
  const result = bankAccount.debitTransaction(1, 150);

  bankAccount.getAccount = jest.fn((id) => ({
    id: id,
    balance: 100,
  }));

  const getAccountResult = bankAccount.getAccount(1);

  // Assert
  expect(result).toBe(false);
  expect(bankAccount.getAccount).toHaveBeenCalledWith(1);
  expect(bankAccount.debitTransaction).toHaveBeenCalledWith(1, 150);
  expect(getAccountResult.balance).toBe(100);
});


In this example

  • In the "arrange" phase, we use Jest's fn function to create a mock function for the debitTransaction function. The mock function checks the value of the amount argument and returns true if it is less than or equal to the current_balance_in_account variable, and false otherwise.
  • In the "act" phase, we invoke the debitTransaction function with the arguments 1 and 50. We also create a mock function for the getAccount function and use it to retrieve an account with an ID of 1 and a balance of 50  after transaction.
  • In the "assert" phase, we use the toBe matcher to check that the result of the debitTransaction function is true, and the toHaveBeenCalledWith matcher to check that the getAccount and debitTransaction functions were called with the correct arguments.
  • We also use the toBe matcher to check that the balance of the retrieved account is 50.

This test case is testing the debitTransaction function to ensure that it correctly updates the balance of an account and returns the correct result based on the input arguments.

This is how we can mimic or mock behaviours of the functions in unit testing

Watching for Tests

If we want a node to watch for changes to our .js files so we don't have to keep entering $ npm test over and over, simply add the following to the test script in package.json, run $ npm test, and the tests will run automatically anytime you make changes to a .js file.



"scripts": {
    "test": "jest --watch *.js"
  }

In this series of blogs, we will soon be talking about Integration testing. To know more reach out to us at contactus@coditation.com.

Hi, I am Sahil Sukhwani. I am a tech-savvy software developer with a passion for creating groundbreaking products, continuously learning & evolving so that I can turn my ideas into reality.

Want to receive update about our upcoming podcast?

Thanks for joining our newsletter.
Oops! Something went wrong.

Latest Articles

Implementing Custom Instrumentation for Application Performance Monitoring (APM) Using OpenTelemetry

Application Performance Monitoring (APM) has become crucial for businesses to ensure optimal software performance and user experience. As applications grow more complex and distributed, the need for comprehensive monitoring solutions has never been greater. OpenTelemetry has emerged as a powerful, vendor-neutral framework for instrumenting, generating, collecting, and exporting telemetry data. This article explores how to implement custom instrumentation using OpenTelemetry for effective APM.

Mobile Engineering
time
5
 min read

Implementing Custom Evaluation Metrics in LangChain for Measuring AI Agent Performance

As AI and language models continue to advance at breakneck speed, the need to accurately gauge AI agent performance has never been more critical. LangChain, a go-to framework for building language model applications, comes equipped with its own set of evaluation tools. However, these off-the-shelf solutions often fall short when dealing with the intricacies of specialized AI applications. This article dives into the world of custom evaluation metrics in LangChain, showing you how to craft bespoke measures that truly capture the essence of your AI agent's performance.

AI/ML
time
5
 min read

Enhancing Quality Control with AI: Smarter Defect Detection in Manufacturing

In today's competitive manufacturing landscape, quality control is paramount. Traditional methods often struggle to maintain optimal standards. However, the integration of Artificial Intelligence (AI) is revolutionizing this domain. This article delves into the transformative impact of AI on quality control in manufacturing, highlighting specific use cases and their underlying architectures.

AI/ML
time
5
 min read