Testing and Debugging
Bob wrote a RESTful API for an e-commerce platform, and every time he added a new feature, something else broke. Manual testing with Postman took 30 minutes per deployment, and he still missed edge cases. He introduced Jest for unit testing and Supertest for API integration testing — now npm test runs 50 tests in 3 seconds, and every deployment is backed by an automated CI check. He also added winston for structured logging, so when a bug slips through, the logs tell him exactly what went wrong.
You will learn:
- Jest unit testing (describe / it / expect / mock)
- Supertest API integration testing (HTTP request simulation)
- Node.js built-in debugger (node inspect / Chrome DevTools)
- winston structured logging (transports / format / rotation)
- Test coverage & TDD workflow
1. Testing Overview
(1) Why Write Tests
| Without Tests | With Tests |
|---|---|
| Manual verification, 30 min each run | Automated, 3 seconds |
| Change one thing, break everything | Change one thing, tests tell you what broke |
| Anxiety before deployment | Green lights, deploy with confidence |
| Afraid to refactor | Refactor with test safety net |
▶ Example: (2) Test Pyramid
graph TD
A["Unit Tests<br/>Many / Fast / Cheap"] --> B["Integration Tests<br/>Moderate / Slower"]
B --> C["E2E Tests<br/>Few / Slow / Expensive"]
style A fill:#4CAF50,color:#fff
style B fill:#FF9800,color:#fff
style C fill:#F44336,color:#fff
- Unit Tests: test individual functions/modules, fast, largest quantity
- Integration Tests: test cooperation between modules (e.g., API + database), moderate speed
- E2E Tests: simulate user operations, slowest, fewest
(3) Node.js Testing Tool Comparison
| Tool | Type | Features | Best For |
|---|---|---|---|
| Jest | Unit + Integration | Zero config, built-in mock/coverage, snapshot testing | General purpose |
| Mocha | Unit + Integration | Flexible, rich plugins, needs chai/sinon | Custom needs |
| Vitest | Unit | Vite ecosystem, native ESM, very fast | Vite projects |
| node:test | Unit | Node.js built-in, zero dependencies | Simple projects |
| Supertest | HTTP testing | HTTP request simulation, test Express routes | API integration tests |
| Playwright | E2E | Browser automation, multi-browser support | Frontend E2E |
This lesson uses the Jest + Supertest combination, the most mainstream testing solution in the Node.js community.
2. Jest Unit Testing
▶ Example: (1) Installation & Setup
npm install --save-dev jest
Add the test script to package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Jest works with zero configuration by default, automatically finding *.test.js or *.spec.js files.
▶ Example: (2) Basic Syntax: describe / it / expect
// math.js — the module to test
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
module.exports = { add, subtract, multiply, divide };
// math.test.js
const { add, subtract, multiply, divide } = require('./math');
describe('Math utilities', () => {
test('add should sum two numbers', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
});
test('subtract should return the difference', () => {
expect(subtract(5, 3)).toBe(2);
});
test('multiply should return the product', () => {
expect(multiply(3, 4)).toBe(12);
});
test('divide should throw on zero divisor', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Run:
npm test
Output:
PASS ./math.test.js
Math utilities
✓ add should sum two numbers (2 ms)
✓ subtract should return the difference
✓ multiply should return the product
✓ divide should throw on zero divisor (1 ms)
Tests: 4 passed, 4 total
Time: 0.5s
(3) Common Matchers
| Matcher | Meaning | Example |
|---|---|---|
toBe |
Strict equality (===) | expect(1 + 1).toBe(2) |
toEqual |
Deep equality | expect(obj).toEqual({ a: 1 }) |
toBeTruthy / toBeFalsy |
Boolean check | expect(flag).toBeTruthy() |
toBeNull / toBeUndefined |
null / undefined | expect(val).toBeNull() |
toThrow |
Throws exception | expect(fn).toThrow() |
toContain |
Array contains | expect([1, 2, 3]).toContain(2) |
toMatch |
Regex match | expect('hello').toMatch(/ell/) |
toHaveLength |
Length check | expect('abc').toHaveLength(3) |
resolves / rejects |
Promise result | expect(promise).resolves.toBe(5) |
toHaveBeenCalled |
Mock called | expect(fn).toHaveBeenCalled() |
▶ Example: (4) Async Testing
// async-functions.js
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: `User_${id}` });
}, 100);
});
}
async function getUserEmail(id) {
const user = await fetchUser(id);
return `${user.name.toLowerCase()}@example.com`;
}
module.exports = { fetchUser, getUserEmail };
// async-functions.test.js
const { fetchUser, getUserEmail } = require('./async-functions');
describe('Async function tests', () => {
test('fetchUser returns user object', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'User_1' });
});
test('getUserEmail returns formatted email', async () => {
const email = await getUserEmail(42);
expect(email).toBe('user_42@example.com');
});
test('fetchUser resolves within 200ms', async () => {
await expect(fetchUser(1)).resolves.toHaveProperty('id', 1);
});
});
(5) Mocking
Mocks isolate external dependencies (databases, HTTP requests, filesystem) so tests only focus on the current module's logic.
// email-service.js
class EmailService {
send(to, subject, body) {
// actual SMTP call — too slow for unit tests
console.log(`Sending email to ${to}: ${subject}`);
return true;
}
}
class UserService {
constructor(emailService) {
this.emailService = emailService;
}
register(name, email) {
// business logic: validate + send welcome email
if (!email.includes('@')) throw new Error('Invalid email');
this.emailService.send(email, 'Welcome', `Hello ${name}!`);
return { name, email, status: 'registered' };
}
}
module.exports = { EmailService, UserService };
// email-service.test.js
const { EmailService, UserService } = require('./email-service');
describe('UserService', () => {
let mockEmailService;
let userService;
beforeEach(() => {
mockEmailService = {
send: jest.fn().mockReturnValue(true),
};
userService = new UserService(mockEmailService);
});
test('register should call emailService.send', () => {
const result = userService.register('Alice', 'alice@example.com');
expect(result).toEqual({
name: 'Alice',
email: 'alice@example.com',
status: 'registered',
});
expect(mockEmailService.send).toHaveBeenCalledWith(
'alice@example.com',
'Welcome',
'Hello Alice!'
);
});
test('register should throw on invalid email', () => {
expect(() => userService.register('Bob', 'invalid')).toThrow('Invalid email');
expect(mockEmailService.send).not.toHaveBeenCalled();
});
test('register should send exactly one email', () => {
userService.register('Charlie', 'charlie@example.com');
expect(mockEmailService.send).toHaveBeenCalledTimes(1);
});
});
▶ Example: Basic Assertions with Jest
test('basic matchers', () => {
expect(2 + 2).toBe(4);
expect({ name: 'Alice' }).toEqual({ name: 'Alice' });
expect([1, 2, 3]).toContain(2);
expect('hello world').toMatch(/world/);
expect(() => { throw new Error('fail'); }).toThrow('fail');
});
Shows the most commonly used Jest matchers: toBe for primitives, toEqual for objects, toContain for arrays, toMatch for strings, and toThrow for error testing.
3. Supertest API Integration Testing
▶ Example: (1) Installation & Principle
npm install --save-dev supertest
Supertest starts an Express application in memory, sends HTTP requests, and asserts on responses — no real port listening needed.
flowchart LR
A["Test File"] -->|"request(app)"| B["Express App<br/>(in-memory)"]
B -->|"response"| A
A -->|"assert"| C["Jest expect"]
▶ Example: (2) Testing Express API
// app.js — the Express app (export without listen)
const express = require('express');
const app = express();
app.use(express.json());
let products = [
{ id: 1, name: 'Laptop', price: 999.99 },
{ id: 2, name: 'Phone', price: 499.99 },
];
app.get('/api/products', (req, res) => {
res.json(products);
});
app.get('/api/products/:id', (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) return res.status(404).json({ error: 'Product not found' });
res.json(product);
});
app.post('/api/products', (req, res) => {
const { name, price } = req.body;
if (!name || price == null) {
return res.status(400).json({ error: 'Name and price are required' });
}
const newProduct = { id: products.length + 1, name, price };
products.push(newProduct);
res.status(201).json(newProduct);
});
module.exports = app;
Note:
app.jsonly exportsapp, it does not callapp.listen()inside the module.listengoes inserver.js, so Supertest can test theappobject directly.
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('Products API', () => {
test('GET /api/products should return product list', async () => {
const response = await request(app).get('/api/products');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
expect(response.body[0]).toHaveProperty('name', 'Laptop');
});
test('GET /api/products/:id should return a single product', async () => {
const response = await request(app).get('/api/products/1');
expect(response.status).toBe(200);
expect(response.body.id).toBe(1);
});
test('GET /api/products/:id should return 404 for missing product', async () => {
const response = await request(app).get('/api/products/999');
expect(response.status).toBe(404);
expect(response.body).toHaveProperty('error', 'Product not found');
});
test('POST /api/products should create a new product', async () => {
const response = await request(app)
.post('/api/products')
.send({ name: 'Tablet', price: 299.99 });
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('name', 'Tablet');
expect(response.body).toHaveProperty('id', 3);
});
test('POST /api/products should return 400 for missing fields', async () => {
const response = await request(app)
.post('/api/products')
.send({ name: 'Incomplete Product' });
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error');
});
});
▶ Example: (3) Testing Authenticated API
// protected-routes.test.js
const request = require('supertest');
const app = require('./app');
describe('Protected API routes', () => {
let token;
beforeAll(async () => {
// login to get JWT token
const response = await request(app)
.post('/api/auth/login')
.send({ username: 'alice', password: 'secret123' });
token = response.body.token;
});
test('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('username', 'alice');
});
test('should reject access without token', async () => {
const response = await request(app).get('/api/profile');
expect(response.status).toBe(401);
});
test('should reject access with invalid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid_token_here');
expect(response.status).toBe(401);
});
});
▶ Example: API Health Check Test
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/health', (req, res) => res.json({ status: 'ok' }));
describe('GET /health', () => {
it('returns 200 with status ok', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
});
});
A minimal API health check test using Supertest — creates a basic Express app, sends a GET request, and asserts the JSON response body and status code.
4. Node.js Debugging
(1) node inspect + Chrome DevTools
Node.js has a built-in debugger — no extra tools required.
node inspect app.js
Then open chrome://inspect in Chrome and click "Open dedicated DevTools for Node".
Common debugging operations:
| Action | DevTools Shortcut | Command Line |
|---|---|---|
| Continue | F8 | c |
| Step over | F10 | n |
| Step into | F11 | s |
| Step out | Shift+F11 | o |
| Set breakpoint | Click line number | setBreakpoint() |
▶ Example: (2) Setting Breakpoints in Code
// debug-demo.js
function calculateDiscount(price, memberLevel) {
debugger; // execution pauses here in inspector
let discount = 0;
if (memberLevel === 'gold') discount = 0.2;
else if (memberLevel === 'silver') discount = 0.1;
const finalPrice = price * (1 - discount);
return finalPrice;
}
console.log(calculateDiscount(100, 'gold'));
console.log(calculateDiscount(100, 'silver'));
console.log(calculateDiscount(100, 'unknown'));
node inspect debug-demo.js
▶ Example: (3) console Debugging Tips
// console debugging methods
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'admin' },
];
// Table display
console.table(users);
// Measure execution time
console.time('database-query');
// ... query logic ...
console.timeEnd('database-query');
// Stack trace
console.trace('Where is this called from?');
// Grouped output
console.group('User Details');
console.log('Name: Alice');
console.log('Role: admin');
console.groupEnd();
5. winston Structured Logging
(1) Why winston Instead of console.log
| console.log | winston |
|---|---|
| No log levels | Supports debug/info/warn/error |
| No file output | Supports Console + File + HTTP and other transports |
| No formatting | Supports JSON / timestamp / custom formats |
| No log rotation | Pair with winston-daily-rotate-file for auto rotation |
| Not recommended for production | Production-grade choice |
▶ Example: (2) Installation & Basic Setup
npm install winston
npm install winston-daily-rotate-file
// logger.js
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'my-app' },
transports: [
// Error logs — separate file
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
maxSize: '20m',
maxFiles: '14d',
}),
// All logs — combined file
new DailyRotateFile({
filename: 'logs/combined-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
}),
],
});
// Development: also log to console with colorized output
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}));
}
module.exports = logger;
▶ Example: (3) Using in an Express Application
// app-with-logger.js
const express = require('express');
const logger = require('./logger');
const app = express();
app.use(express.json());
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
});
});
next();
});
app.get('/api/health', (req, res) => {
logger.debug('Health check requested');
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id;
logger.info('User lookup', { userId });
if (isNaN(userId)) {
logger.warn('Invalid user ID format', { userId });
return res.status(400).json({ error: 'Invalid user ID' });
}
// Simulate user lookup
const user = { id: parseInt(userId), name: 'Alice' };
logger.info('User found', { userId: user.id, name: user.name });
res.json(user);
});
app.use((err, req, res, next) => {
logger.error('Unhandled error', { error: err.message, stack: err.stack });
res.status(500).json({ error: 'Internal server error' });
});
module.exports = app;
Log file output example:
{"level":"info","message":"HTTP request","service":"my-app","timestamp":"2026-07-13 10:30:00","method":"GET","url":"/api/users/1","status":200,"duration":"15ms"}
{"level":"warn","message":"Invalid user ID format","service":"my-app","timestamp":"2026-07-13 10:30:05","userId":"abc"}
{"level":"error","message":"Unhandled error","service":"my-app","timestamp":"2026-07-13 10:31:00","error":"Cannot read property 'name' of undefined","stack":"TypeError: Cannot read property..."}
▶ Example: Structured Logging with winston
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
logger.info('Server started', { port: 3000, env: 'development' });
logger.warn('Memory usage high', { used: '850MB' });
logger.error('Database connection failed', { db: 'main', retry: 3 });
Demonstrates structured JSON logging with winston — different log levels (info, warn, error) and contextual metadata attached to each log entry.
6. Test Coverage & TDD
(1) Test Coverage
Jest includes Istanbul for coverage reporting:
npm run test:coverage
Output:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 90.5 | 85.71 | 100 | 90.5 |
math.js | 100 | 100 | 100 | 100 |
app.js | 88.24 | 83.33 | 100 | 88.24 | 45-48
----------|---------|----------|---------|---------|-------------------
Coverage dimensions explained:
| Dimension | Meaning | Suggested Target |
|---|---|---|
| Statements | Percentage of statements executed | ≥ 80% |
| Branches | Percentage of condition branches executed | ≥ 75% |
| Functions | Percentage of functions called | ≥ 85% |
| Lines | Percentage of code lines executed | ≥ 80% |
Chasing 100% coverage has diminishing returns; 80% coverage catches most bugs.
▶ Example: (2) TDD Workflow (Red-Green-Refactor)
flowchart LR
A["🔴 Red<br/>Write failing test"] --> B["🟢 Green<br/>Write minimal code to pass"]
B --> C["🔵 Refactor<br/>Improve code while tests pass"]
C --> A
- Red: Write a test first (it will fail because the feature doesn't exist yet)
- Green: Write the minimum code to make the test pass
- Refactor: Improve the code while tests still pass
▶ Example: TDD String Utility Functions
// string-utils.test.js — write test FIRST (Red)
const { capitalize, truncate, slugify } = require('./string-utils');
describe('StringUtils', () => {
describe('capitalize', () => {
test('should capitalize first letter', () => {
expect(capitalize('hello')).toBe('Hello');
});
test('should handle empty string', () => {
expect(capitalize('')).toBe('');
});
test('should handle already capitalized', () => {
expect(capitalize('Hello')).toBe('Hello');
});
});
describe('truncate', () => {
test('should truncate long strings', () => {
expect(truncate('Hello World', 5)).toBe('Hello...');
});
test('should not truncate short strings', () => {
expect(truncate('Hi', 10)).toBe('Hi');
});
});
describe('slugify', () => {
test('should convert to URL slug', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
test('should handle special characters', () => {
expect(slugify('C# & .NET!')).toBe('c-and-net');
});
});
});
// string-utils.js — write implementation AFTER test (Green)
function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
function truncate(str, maxLength) {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + '...';
}
function slugify(str) {
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
module.exports = { capitalize, truncate, slugify };
7. Comprehensive Example: Writing a Complete Test Suite for a Task Management API
▶ Example: Tasks API Test Suite
Step 1 — Application Code
// tasks-app.js
const express = require('express');
const app = express();
app.use(express.json());
let tasks = [];
let nextId = 1;
function resetTasks() {
tasks = [];
nextId = 1;
}
app.get('/api/tasks', (req, res) => {
const { status, priority } = req.query;
let filtered = tasks;
if (status) filtered = filtered.filter(t => t.status === status);
if (priority) filtered = filtered.filter(t => t.priority === priority);
res.json(filtered);
});
app.get('/api/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
app.post('/api/tasks', (req, res) => {
const { title, priority = 'medium' } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' });
const task = { id: nextId++, title, priority, status: 'pending' };
tasks.push(task);
res.status(201).json(task);
});
app.patch('/api/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
const { title, status, priority } = req.body;
if (title) task.title = title;
if (status) task.status = status;
if (priority) task.priority = priority;
res.json(task);
});
app.delete('/api/tasks/:id', (req, res) => {
const index = tasks.findIndex(t => t.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: 'Task not found' });
const deleted = tasks.splice(index, 1);
res.json(deleted[0]);
});
module.exports = { app, resetTasks };
Step 2 — Complete Test Suite
// tasks-app.test.js
const request = require('supertest');
const { app, resetTasks } = require('./tasks-app');
beforeEach(() => {
resetTasks();
});
describe('Tasks API — CRUD operations', () => {
test('should create a task', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ title: 'Write tests', priority: 'high' });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
id: 1,
title: 'Write tests',
priority: 'high',
status: 'pending',
});
});
test('should reject task without title', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ priority: 'high' });
expect(response.status).toBe(400);
expect(response.body.error).toBe('Title is required');
});
test('should list all tasks', async () => {
await request(app).post('/api/tasks').send({ title: 'Task 1' });
await request(app).post('/api/tasks').send({ title: 'Task 2' });
const response = await request(app).get('/api/tasks');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
});
test('should filter tasks by status', async () => {
await request(app).post('/api/tasks').send({ title: 'Pending task' });
const createRes = await request(app).post('/api/tasks').send({ title: 'Done task' });
await request(app).patch(`/api/tasks/${createRes.body.id}`).send({ status: 'done' });
const response = await request(app).get('/api/tasks?status=done');
expect(response.body).toHaveLength(1);
expect(response.body[0].status).toBe('done');
});
test('should update a task', async () => {
const createRes = await request(app).post('/api/tasks').send({ title: 'Old title' });
const response = await request(app)
.patch(`/api/tasks/${createRes.body.id}`)
.send({ title: 'New title', status: 'done' });
expect(response.status).toBe(200);
expect(response.body.title).toBe('New title');
expect(response.body.status).toBe('done');
});
test('should delete a task', async () => {
const createRes = await request(app).post('/api/tasks').send({ title: 'To delete' });
const response = await request(app).delete(`/api/tasks/${createRes.body.id}`);
expect(response.status).toBe(200);
const listRes = await request(app).get('/api/tasks');
expect(listRes.body).toHaveLength(0);
});
test('should return 404 for non-existent task', async () => {
const response = await request(app).get('/api/tasks/999');
expect(response.status).toBe(404);
});
});
Run:
npm test
Output:
PASS ./tasks-app.test.js
Tasks API — CRUD operations
✓ should create a task (15 ms)
✓ should reject task without title (3 ms)
✓ should list all tasks (4 ms)
✓ should filter tasks by status (8 ms)
✓ should update a task (5 ms)
✓ should delete a task (5 ms)
✓ should return 404 for non-existent task (2 ms)
Tests: 7 passed, 7 total
Time: 0.8s
❓ FAQ
info, only info and above are output.node --inspect-brk node_modules/.bin/jest --runInBand, then debug in Chrome DevTools. --runInBand ensures Jest runs on a single thread so breakpoints work.http.createServer(app) — no port is occupied, and it closes automatically when tests finish.📖 Summary
- Test pyramid: Unit tests (many, fast, cheap) → Integration tests (moderate) → E2E tests (few, slow, expensive)
- Jest zero-config unit testing: describe/it/expect + mocks for external dependencies
- Supertest tests Express API in memory: request(app).get/post/patch/delete
- Node.js built-in
node inspectdebugger with Chrome DevTools visual breakpoints - winston provides structured logging: multiple levels + multiple transports + log rotation
- 80%+ test coverage is sufficient; TDD workflow Red→Green→Refactor improves code quality
📝 Exercises
- Write Jest unit tests for the following function:
function isPalindrome(str) { return str === str.split('').reverse().join(''); }, covering normal palindromes, non-palindromes, empty strings, and mixed case scenarios - Create an Express API (CRUD for
/api/books), write at least 5 integration tests using Supertest (create, read, update, delete, 404) - Add winston logging middleware to an existing Express app, logging method/url/status/duration for every request, with daily log rotation
- Use TDD to develop a
formatCurrency(amount, currency)function: first write a failing test (e.g.,formatCurrency(1234.5, 'USD')→'$1,234.50'), then write the implementation - Run
jest --coverage, find files with coverage below 80%, and add test cases to meet the coverage target



