Skip to main content

API Testing in JavaScript: Choosing the Right Frameworks (Explained for Beginners)

Photo by Paul Esch-Laurent on Unsplash


When it comes to testing APIs in JavaScript, there are various frameworks available. Each serves different purposes, making it important to understand whatwherewhen, and why to use them. In this guide, we’ll explore API testing using two powerful combinations: Jest + Supertest and Mocha + Chai + Sinon, with simple code examples to get you started.

What is API Testing?

API testing involves sending requests to your API endpoints and checking whether the responses match expectations. This ensures that your API behaves correctly, handling various actions like fetching, creating, or deleting data.

Why Use Testing Frameworks for API Testing?

  • Ensure Correct Responses: When your API is called, it should respond with the correct data or error message.
  • Catch Bugs Early: API testing helps find problems in your server logic before releasing the code to production.
  • Automate the Process: You can run tests automatically, saving time and ensuring that everything still works after updates.
  • Confidence in Refactoring: Refactoring your API is less risky when you have tests to verify everything works as expected.

When and Where Should You Use API Testing Frameworks?

  • When: You should write API tests during development, after creating or updating your API endpoints.
  • Where: You can test various parts of your API, like data retrieval (GET), data creation (POST), or data updates (PUT).

Popular API Testing Frameworks

There are many frameworks for API testing, but here are two effective combinations:

  • Jest + Supertest: Simple setup for API testing, great for beginners.
  • Mocha + Chai + Sinon: Offers flexibility and powerful tools for more complex tests.

Prerequisites: Setting Up Your Environment

Before diving into API testing, it’s important to ensure that your development environment is properly set up. Here’s what you need to do:

Step 1: Install Node.js and npm

Node.js is a JavaScript runtime that allows you to run JavaScript code on the server side. It comes with npm (Node Package Manager), which helps you install and manage packages.

Installation Steps:

  • Download and install Node.js from the official website.
  • To verify the installation, open your terminal and run:
node -v
npm -v

This should display the installed versions of Node.js and npm.

Step 2: Create a New Project Directory

Open your terminal and create a new directory for your project. Navigate into that directory:

mkdir api-testing-example
cd api-testing-example

Alternatively, you can create a new directory using the file explorer and then open the terminal in that location.

Step 3: Initialize a New Node.js Project

Run the following command to create a package.json file, which will manage your project dependencies:

npm init -y

The -y flag automatically answers "yes" to all prompts, creating a default package.json file.

Step 4: Open the Project Directory in Your Code Editor

Open the project directory you just created in your code editor of choice. If you do not have one yet, I recommend using Visual Studio Code (VS Code). Additionally, you can explore the following extensions in VS Code that are tailored for testing frameworks:

Jest by Orta — Visual Studio Code Extension
Mocha Test Explorer by Holger Benl — Visual Studio Code Extension

Getting Started: Jest + Supertest

We’ll first look at Jest (a popular testing framework) and Supertest (a library for testing HTTP endpoints).

Step 1: Set Up a Simple API

First, let’s build a simple API using Express, a framework for building web servers.

1. Install dependencies:

npm install express jest supertest --save-dev

2. Create a file app.js for the API:

// app.js
const express = require('express');
const app = express();

app.use(express.json());

const users = [{ id: 1, name: 'John Doe' }];

app.get('/users', (req, res) => {
res.status(200).json(users);
});

app.post('/users', (req, res) => {
const newUser = { id: users.length + 1, name: req.body.name };
users.push(newUser);

res.status(201).json(newUser);
});

module.exports = app;

This API has two endpoints:

  • GET /users: Returns a list of users.
  • POST /users: Adds a new user.

Step 2: Write API Tests Using Jest + Supertest

Now, let’s write tests for this API.

1. Create the test file app.test.js:

// app.test.js
const request = require('supertest');
const app = require('./app');

describe('API Tests', () => {
it('GET /users should return a list of users', async () => {
const res = await request(app).get('/users');

expect(res.statusCode).toBe(200);
expect(res.body.length).toBeGreaterThan(0);
});

it('POST /users should create a new user', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Jane Doe' });

expect(res.statusCode).toBe(201);
expect(res.body.name).toBe('Jane Doe');
});
});

Step 3: Run the Tests

You can now run the tests:

npx jest

Jest will run your tests and show whether the API is working as expected.

Understanding the Terms

  • Jest: A testing framework that allows you to write and run tests.
  • Supertest: A library used for testing HTTP requests like GETPOST, etc.
  • describe: Groups tests together. For example, you can group all API tests under describe('API Tests').
  • it: Defines an individual test. For example, it('GET /users should return a list of users').
  • expect: Checks if the result matches your expectation. For example, expect(res.statusCode).toBe(200) checks if the response status is 200 (OK).

Advanced API Testing: Mocha + Chai + Sinon

If you need more flexibility, you can use Mocha (a testing framework), Chai (an assertion library), and Sinon (a library for mocking/stubbing).

Step 1: Set Up a Simple API

We’ll use the same API as before.

Step 2: Write API Tests Using Mocha + Chai

1. Install dependencies:

npm install mocha chai sinon supertest --save-dev

2. Create the test file app.test.js:

// app.test.js
const request = require('supertest');
const { expect } = require('chai');
const sinon = require('sinon');
const app = require('./app');

describe('API Tests', () => {
it('GET /users should return a list of users', (done) => {
request(app)
.get('/users')
.end((err, res) => {
expect(res.statusCode).to.equal(200);
expect(res.body.length).to.be.greaterThan(0);
done();
});
});

it('POST /users should create a new user', (done) => {
request(app)
.post('/users')
.send({ name: 'Jane Doe' })
.end((err, res) => {
expect(res.statusCode).to.equal(201);
expect(res.body.name).to.equal('Jane Doe');
done();
});
});
});

Step 3: Using Sinon (optional)

What is Sinon?

Sinon is a library that helps you mockspy, or stub functions in your code. It’s useful when you want to:

  • Stub external dependencies (e.g., databases, third-party APIs) to simulate their behavior without actually making calls to them.
  • Spy on how functions are called, keeping track of how many times they were called and with what arguments.
  • Mock behavior to simulate different responses or actions for testing purposes.

How Could Sinon Be Used in This Example?

In your test, you might want to avoid actually making changes to the real API or database. You can use Sinon to stub the logic inside your API to control how it behaves during testing.

Example: Stubbing a Database Call with Sinon

Let’s say you have a users array in your API and you want to simulate adding a user without modifying the real data. You can use Sinon to stub the part of the code that handles adding users.

1. Modify the API to extract user operations: Let’s say the user creation logic is inside a function addUser:

// app.js
const express = require('express');
const app = express();
app.use(express.json());

let users = [{ id: 1, name: 'John Doe' }];

const addUser = (name) => {
const newUser = { id: users.length + 1, name };
users.push(newUser);
return newUser;
};

app.get('/users', (req, res) => {
res.status(200).json(users);
});

app.post('/users', (req, res) => {
const newUser = addUser(req.body.name);
res.status(201).json(newUser);
});

module.exports = { app, addUser }; // Export addUser for testing

2. Stub the addUser function using Sinon: In your test, you can now stub the addUser function so it behaves differently during the test:

// app.test.js
const request = require('supertest');
const { expect } = require('chai');
const sinon = require('sinon');
const { app, addUser } = require('./app'); // Import addUser

describe('API Tests with Sinon', () => {
let addUserStub;

beforeEach(() => {
// Stub addUser before each test
addUserStub = sinon.stub().returns({ id: 999, name: 'Stubbed User' });
});

it('POST /users should create a new user with stubbed data', (done) => {
sinon.replace(addUser, 'addUser', addUserStub); // Replace the original function with the stub

request(app)
.post('/users')
.send({ name: 'Jane Doe' })
.end((err, res) => {
expect(res.statusCode).to.equal(201);
expect(res.body.id).to.equal(999); // Check the stubbed data
expect(res.body.name).to.equal('Stubbed User');
done();
});

sinon.restore(); // Restore the original addUser function after the test
});

afterEach(() => {
sinon.restore(); // Clean up after each test
});
});

Explanation:

  • sinon.stub(): This creates a stub for the addUser function. A stub replaces a function with a custom implementation—in this case, it always returns { id: 999, name: 'Stubbed User' }.
  • sinon.replace(): Replaces the actual addUser function in the API with the stub for this test.
  • sinon.restore(): Restores the original function after the test is done, to avoid interfering with other tests.

Use Cases for Sinon:

  • Mocking dependencies: If your API depends on a database or an external service, you can mock those dependencies during tests to avoid making real API calls or database changes.
  • Spying on function calls: You can spy on functions to check how many times they were called or what arguments they received.
  • Simulating errors: You can make a function throw an error to test how your API handles failure scenarios.

Step 4: Run the Tests

Run the tests using:

npx mocha

Understanding the Terms

  • Mocha: A testing framework that runs your tests.
  • Chai: An assertion library that lets you write human-readable checks like expect(res.statusCode).to.equal(200).
  • Sinon: A tool for mocking or stubbing, useful for simulating API responses without actually hitting the real server.
  • done: A callback used in asynchronous tests (e.g., making an API request) to signal when the test is finished.

When to Use These Frameworks

  • Jest + Supertest: Best for beginners and when you want a simple, easy-to-setup solution for API testing.
  • Mocha + Chai + Sinon: Ideal for more complex testing needs where you might need to mock or stub parts of your API.

Conclusion

Both Jest + Supertest and Mocha + Chai + Sinon are powerful combinations for API testing. While Jest is great for quick and simple tests, Mocha with Chai and Sinon offers more flexibility for advanced scenarios. Try the code examples above to get a feel for API testing and choose the setup that works best for your project!

Comments

Popular posts from this blog

Understanding Number Systems: Decimal, Binary, and Hexadecimal

In everyday life, we use numbers all the time, whether for counting, telling time, or handling money. The number system we’re most familiar with is the   decimal system , but computers use other systems, such as   binary   and   hexadecimal . Let’s break down these number systems to understand how they work. What is a Number System? A number system is a way of representing numbers using a set of symbols and rules. The most common number systems are: Decimal (Base 10) Binary (Base 2) Hexadecimal (Base 16) Each system has a different “base” that tells us how many unique digits (symbols) are used to represent numbers. Decimal Number System (Base 10) This is the system we use daily. It has  10 digits , ranging from  0 to 9 . Example: The number  529  in decimal means: 5 × 1⁰² + 2 × 1⁰¹ + 9 × 1⁰⁰ =  500 + 20 + 9 = 529 Each position represents a power of 10, starting from the rightmost digit. Why Base 10? Decimal is base 10 because it has 10 digits...

How to Monetize Your API as an Individual Developer While Hosting on Your Own Server?

In the API economy, cloud services like AWS, Google Cloud, and Azure offer many conveniences, such as scaling and infrastructure management. However, some developers prefer more control and autonomy, opting to host their APIs on personal servers. Whether for cost efficiency, data privacy, or customization, hosting your own API comes with both advantages and challenges. But, even without cloud platforms, there are effective ways to monetize your API. This guide will explore how individual developers can successfully monetize their APIs while hosting them on their own servers. Why Host Your API on Your Own Server? Hosting your own API gives you full control over the infrastructure and potentially lower long-term costs. Here’s why some developers choose this approach: Cost Control : Instead of paying ongoing cloud fees, you may opt for a one-time or lower-cost hosting solution that fits your budget and resource needs. Data Ownership : You have full control over data, which is critical if ...

API Testing with Jest and Supertest: A Step-by-Step Guide

API testing is essential to ensure your endpoints behave as expected across all scenarios. In this guide, we’ll explore how to use Jest and Supertest to test a sample API with various response types, including success, authentication errors, and validation errors. By the end, you’ll understand how to apply these tools to check for different response structures and status codes. 0. Prerequisites: Setting Up Your Environment Before diving into API testing, it’s important to ensure that your development environment is properly set up. Here’s what you need to do: Step 1: Install Node.js and npm Node.js  is a JavaScript runtime that allows you to run JavaScript code on the server side. It comes with  npm  (Node Package Manager), which helps you install and manage packages. Installation Steps: Download and install Node.js from the  official website . To verify the installation, open your terminal and run: node -v npm -v This should display the installed versions of Node.js...

The Weight of Responsibility: A Developer’s Journey to Balance Passion and Reality

For the past several years, Eddie has been on a steady climb in his career as a developer, but recently, he found himself at a crossroads — caught between the weight of his responsibilities and the desire to pursue his true passions. His journey began with a three-month internship as a web developer, which led to nearly four years in an application developer role. After that, he spent almost a year as a systems associate, managing tasks across systems analysis, quality assurance, and business analysis. Eventually, he returned to full-time software development for another two years before transitioning into more complex roles. For over a year, he worked as a multi-role software developer and database administrator before stepping into his current position as a senior software developer, database administrator, and cloud administrator — occasionally handling security tasks as well. Now, with over 8 years of professional experience, he also leads a small team of developers, which has been...

Avoiding Confusion in API Design: The Importance of Clear Responses

In today’s fast-paced software development landscape, APIs play a crucial role in connecting services and enabling functionality. However, poor design choices can lead to confusion and inefficiency for both developers and users. One such choice is the omission of a response body for successful requests, a practice I recently encountered in an enterprise API designed for bill payments. The Case of the No-Response API The API in question serves two main endpoints: one for inquiring about account validity and another for confirming payment. When successful, the API returned a  200 OK  status but no response body. This design choice led to significant confusion during our integration process. Even the internal team who developed the said API struggled to justify this approach, revealing a lack of clarity around the rationale behind it. Pros of This Design Choice While the intention behind this design may have been to streamline responses, several potential benefits can be identifi...