

This article will guide you through writing and running Unit Tests for RESTful API that uses Node.js, Express, Prisma ORM and MySQL using testing tools Jest and Supertest
1. Objectives#
- Learn how to install and configure Jest and Supertest
- Understand the principles of writing Unit Tests for API
- Test CRUD Operations functionality
- Verify HTTP Status Code and Response Data
- Run automated testing
2. Testing Tools Used#
2.1 Jest#
Jest is a JavaScript Testing Framework developed by Facebook
Advantages:
- Easy to use, no complex configuration required
- Built-in Assertion Library
- Supports Async/Await
- Clear and readable test result display
- Includes Code Coverage Report
2.2 Supertest#
Supertest is a Library for testing HTTP Server
Advantages:
- Works well with Express
- Supports all types of HTTP Requests
- Comprehensive Response verification
- Systematic Test Case writing
3. Project Structure#
Create a new project and install Dependencies
mkdir api-testing-demo
cd api-testing-demo
npm init -y
bash3.1 Install Dependencies#
# Install Production Dependencies
npm install express body-parser cors @prisma/client
# Install Development Dependencies
npm install -D jest supertest prisma
bash3.2 File Structure#
api-testing-demo/
├── prisma/
│ └── schema.prisma
├── src/
│ └── prisma.js
├── tests/
│ └── api.test.js
├── index.js
├── package.json
└── .env
plaintext4. Database and Prisma Configuration#
4.1 Create .env file#
DATABASE_URL="mysql://username:password@localhost:3306/test_db"
plaintext4.2 Configure Prisma Schema#
Create file prisma/schema.prisma
:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
fname String
lname String
username String @unique
email String @unique
avatar String?
}
prisma4.3 Create Prisma Client#
Create file src/prisma.js
:
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
module.exports = prisma;
javascript4.4 Create Database#
npx prisma migrate dev --name init
npx prisma generate
bash5. Create API Server#
Create file index.js
:
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const prisma = require('./src/prisma');
const app = express();
const port = 5000;
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Routes
app.get('/', (req, res) => {
res.send('Hello! RESTful API is ready to use with Prisma')
});
// Get all users
app.get('/users', async (req, res) => {
const users = await prisma.user.findMany();
res.json(users);
});
// Get user by ID
app.get('/users/:id', async (req, res) => {
const id = parseInt(req.params.id);
const user = await prisma.user.findUnique({ where: { id } });
if (!user) return res.status(404).json({ message: 'User not found' });
res.json(user);
});
// Create new user
app.post('/users', async (req, res) => {
try {
const newUser = await prisma.user.create({ data: req.body });
res.status(201).json(newUser);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Update user
app.put('/users/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
const updated = await prisma.user.update({
where: { id },
data: req.body
})
res.json(updated);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Delete user
app.delete('/users/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
const deleted = await prisma.user.delete({ where: { id } });
res.json({ message: `User with ID ${deleted.id} deleted` })
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Start server only when run directly (not in tests)
if (require.main === module) {
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
}
// Export for testing
module.exports = app;
javascriptImportant Code Explanation:
require('./src/prisma')
- Import Prisma Clientprisma.user.findMany()
- Retrieve all usersprisma.user.findUnique()
- Retrieve user by IDprisma.user.create()
- Create new userprisma.user.update()
- Update user dataprisma.user.delete()
- Delete userif (require.main === module)
- Start server only when running file directlymodule.exports = app
- Export application for testing
6. Write Test Cases#
Create file tests/api.test.js
:
const request = require('supertest');
const app = require('../index');
describe('Prisma API Tests', () => {
let createdUserId;
// Test 1: Test Root Endpoint
it('GET / should return API ready message', async () => {
const res = await request(app).get('/');
expect(res.statusCode).toBe(200);
expect(res.text).toContain('RESTful API is ready');
});
// Test 2: Test creating a user
it('POST /users should create a user', async () => {
const res = await request(app)
.post('/users')
.send({
fname: 'John',
lname: 'Smith',
username: `john_smith_${Date.now()}`, // unique username
email: `john${Date.now()}@example.com`, // unique email
avatar: 'https://example.com/avatar.jpg'
});
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('id');
createdUserId = res.body.id; // Save ID for later tests
});
// Test 3: Test retrieving the created user
it('GET /users/:id should return the created user', async () => {
const res = await request(app).get(`/users/${createdUserId}`);
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('fname', 'John');
expect(res.body).toHaveProperty('lname', 'Smith');
});
// Test 4: Test updating the user
it('PUT /users/:id should update the user', async () => {
const res = await request(app)
.put(`/users/${createdUserId}`)
.send({ fname: 'Jackson', lname: 'Mars' });
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('fname', 'Jackson');
expect(res.body).toHaveProperty('lname', 'Mars');
});
// Test 5: Test deleting the user
it('DELETE /users/:id should delete the user', async () => {
const res = await request(app).delete(`/users/${createdUserId}`);
expect(res.statusCode).toBe(200);
expect(res.body.message).toContain(`User with ID ${createdUserId} deleted`);
});
// Test 6: Test retrieving a deleted user
it('GET /users/:id should return 404 for deleted user', async () => {
const res = await request(app).get(`/users/${createdUserId}`);
expect(res.statusCode).toBe(404);
expect(res.body).toHaveProperty('message', 'User not found');
});
});
javascript7. Test Cases Explanation for Each Section#
7.1 Importing Dependencies#
const request = require('supertest');
const app = require('../index');
javascriptsupertest
- For sending HTTP Requests to Express App../index
- Import Express Application to be tested
7.2 Test Suite Structure#
describe('Prisma API Tests', () => {
let createdUserId; // Variable to store the ID of the created user
// Test cases will be here
});
javascriptdescribe()
- Group related Test Cases togethercreatedUserId
- Store ID of created data for use in subsequent tests
7.3 Testing Each Endpoint#
Test 1: Root Endpoint
it('GET / should return API ready message', async () => {
const res = await request(app).get('/');
expect(res.statusCode).toBe(200);
expect(res.text).toContain('RESTful API is ready');
});
javascriptit()
- Define individual Test Caserequest(app).get('/')
- Send GET Request to Root Pathexpect(res.statusCode).toBe(200)
- Verify Status Codeexpect(res.text).toContain()
- Verify content in Response
Test 2: User Creation
it('POST /users should create a user', async () => {
const res = await request(app)
.post('/users')
.send({
fname: 'John',
lname: 'Smith',
username: `john_smith_${Date.now()}`,
email: `john${Date.now()}@example.com`,
avatar: 'https://example.com/avatar.jpg'
});
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('id');
createdUserId = res.body.id;
});
javascript.send()
- Send data in Request BodyDate.now()
- Create Unique Value to avoid duplicate datatoHaveProperty('id')
- Verify that Response has that Property- Store
id
for use in subsequent tests
7.4 Commonly Used Jest Matchers#
Matcher | Meaning | Example |
---|---|---|
.toBe() | Equals (===) | expect(status).toBe(200) |
.toEqual() | Equals (deep equality) | expect(obj).toEqual({id: 1}) |
.toContain() | Contains text/value | expect(text).toContain('success') |
.toHaveProperty() | Has that Property | expect(obj).toHaveProperty('name') |
.toBeNull() | Is null | expect(value).toBeNull() |
.toBeTruthy() | Is truthy | expect(result).toBeTruthy() |
8. Configure NPM Scripts#
Add to package.json
:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"start": "node index.js"
},
"jest": {
"testEnvironment": "node",
"testTimeout": 10000
}
}
jsonScripts Explanation:
npm test
- Run all testsnpm run test:watch
- Run tests and watch for file changesnpm run test:coverage
- Run tests with Code Coverage ReporttestTimeout: 10000
- Set timeout to 10 seconds (for Database Operations)
9. Run Tests#
9.1 Initialize Database#
# Create database and tables
npx prisma migrate dev --name init
# Generate Prisma Client
npx prisma generate
bash9.2 Run Tests#
# Run tests once
npm test
# Run tests in Watch Mode
npm run test:watch
# Run tests with Coverage Report
npm run test:coverage
bash9.3 Expected Test Results#
PASS tests/api.test.js
Prisma API Tests
√ GET / should return API ready message (26 ms)
√ POST /users should create a user (154 ms)
√ GET /users/:id should return the created user (20 ms)
√ PUT /users/:id should update the user (13 ms)
√ DELETE /users/:id should delete the user (8 ms)
√ GET /users/:id should return 404 for deleted user (6 ms)
Test Suites: 1 passed, 1 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 0.829 s, estimated 1 s
Ran all test suites.
plaintext10. Additional Error Cases Testing#
Add Test Cases for error scenarios:
describe('Error Handling Tests', () => {
// Test creating a user with incomplete data
it('POST /users should return 500 for invalid data', async () => {
const res = await request(app)
.post('/users')
.send({
fname: 'John'
// Missing lname, username, email
});
expect(res.statusCode).toBe(500);
expect(res.body).toHaveProperty('message');
});
// Test retrieving a user that does not exist
it('GET /users/999999 should return 404', async () => {
const res = await request(app).get('/users/999999');
expect(res.statusCode).toBe(404);
expect(res.body).toHaveProperty('message', 'User not found');
});
// Test creating a user with a duplicate email
it('POST /users should return 500 for duplicate email', async () => {
const userData = {
fname: 'Jane',
lname: 'Doe',
username: 'jane_doe_test',
email: 'duplicate@test.com',
avatar: 'https://example.com/avatar.jpg'
};
// Create user the first time
await request(app).post('/users').send(userData);
// Attempt to create another user with the same email
const res = await request(app).post('/users').send({
...userData,
username: 'jane_doe_test2' // Change username but keep the same email
});
expect(res.statusCode).toBe(500);
expect(res.body.message).toContain('email');
});
});
javascriptExpected Test Results
PASS tests/api.test.js
Prisma API Tests
√ GET / should return API ready message (30 ms)
√ POST /users should create a user (28 ms)
√ GET /users/:id should return the created user (7 ms)
√ PUT /users/:id should update the user (8 ms)
√ DELETE /users/:id should delete the user (8 ms)
√ GET /users/:id should return 404 for deleted user (5 ms)
Error Handling Tests
√ POST /users should return 500 for invalid data (133 ms)
√ GET /users/999999 should return 404 (6 ms)
√ POST /users should return 500 for duplicate email (17 ms)
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total
Snapshots: 0 total
Time: 0.686 s, estimated 1 s
Ran all test suites.
plaintext11. Code Coverage#
Check Code Coverage to see how much of the code is tested:
npm run test:coverage
bashDisplayed Results:
PASS tests/api.test.js
Prisma API Tests
√ GET / should return API ready message (25 ms)
√ POST /users should create a user (28 ms)
√ GET /users/:id should return the created user (6 ms)
√ PUT /users/:id should update the user (8 ms)
√ DELETE /users/:id should delete the user (8 ms)
√ GET /users/:id should return 404 for deleted user (6 ms)
Error Handling Tests
√ POST /users should return 500 for invalid data (141 ms)
√ GET /users/999999 should return 404 (5 ms)
√ POST /users should return 500 for duplicate email (16 ms)
----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 86.04 | 75 | 71.42 | 85.71 |
api-prisma | 85 | 75 | 71.42 | 84.61 |
index.js | 85 | 75 | 71.42 | 84.61 | 17-18,46,56,61-62
api-prisma/src | 100 | 100 | 100 | 100 |
prisma.js | 100 | 100 | 100 | 100 |
----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total
Snapshots: 0 total
Time: 0.749 s, estimated 1 s
Ran all test suites.
plaintextExplanation:
- % Stmts - Percentage of Statements tested
- % Branch - Percentage of Conditional Branches tested
- % Funcs - Percentage of Functions tested
- % Lines - Percentage of code lines tested
12. Best Practices for API Testing#
✅ What to do:
- Use unique data in each test (e.g., timestamp)
- Test both Success and Error Cases
- Clean up data after testing
- Write understandable Test Cases
- Use
describe()
to group Test Cases - Verify both Status Code and Response Data
❌ What to avoid:
- Use hard-coded data that might duplicate
- Write Test Cases that depend too much on each other
- Not testing Error Cases
- Use unclear Test Case names
- Not verifying complete Response
API testing is a crucial part of modern software development. These tools help ensure development quality and reliability