Test-Driven Development (TDD)
What is TDD?
Test-Driven Development (TDD) is a software development practice where tests are written before the actual implementation. The process follows a cycle:
- Write a test – Define what the function or feature should do.
- Run the test – Ensure it fails since the functionality is not implemented yet.
- Write the code – Implement the minimum code required to pass the test.
- Refactor – Improve the code while ensuring the test still passes.
- Repeat – Continue adding more tests and refining the code.
Why Use TDD?
- Reliability: Ensures your code meets the intended requirements.
- Maintainability: Encourages better design and cleaner code.
- Bug Prevention: Catches issues early, reducing debugging time later.
- Confidence in Refactoring: Allows developers to modify code without fear of breaking existing functionality.
- Improved Collaboration: Provides clear specifications for team members.
How to Use TDD in TypeScript with Playwright
Part 1: Setting Up Playwright
Steps to Follow:
- Install Playwright and NYC for test coverage:
npm install -D @playwright/test nyc
- Create a
playwright.config.tsfile with the following configuration:
import { defineConfig } from '@playwright/test';
import fs from 'fs';
import path from 'path';
// Function to dynamically scan test directories
const testRootDir = 'tests/'; // Adjust if needed
const testDirs = fs
.readdirSync(testRootDir, { withFileTypes: true })
.filter(dir => dir.isDirectory() && dir.name.startsWith('test-')) // Include only test folders
.map(dir => ({
name: dir.name,
testDir: path.join(testRootDir, dir.name),
}));
export default defineConfig({
testDir: '.', // Root directory
timeout: 30000, // 30s timeout
expect: { timeout: 5000 }, // Assertion timeout
fullyParallel: false,
workers: 2, // Adjust based on system resources
reporter: [['list'], ['html', { outputFile: 'test-results.html' }]], // Reporters
globalSetup: require.resolve('./tests/global-setup'), // ✅ Adjusted path
globalTeardown: require.resolve('./tests/global-teardown'), // ✅ Adjusted path
use: {
baseURL: 'https://your-api-url.com', // Set your API base URL
extraHTTPHeaders: {
'Content-Type': 'application/json',
'Authorization': process.env.SDST_TOKEN || 'invalid', // Use env variables instead of hardcoding
},
ignoreHTTPSErrors: true,
},
projects: testDirs, // Dynamically generated projects
});
- Update
package.jsonto include the test script:
"scripts": {
"test": "nyc --reporter=html --reporter=text playwright test"
}
Part 2: Configuring Pre-requisites
*Create a tests directory
Setting Up the Database
Since our backend uses AWS RDS/MongoDB inside a VPC, we cannot directly call the handlers as the connection would fail. To resolve this, we create mock databases inside a Docker container for testing purposes.
docker-compose.yaml
Create a docker-compose.yaml file to set up PostgreSQL and MongoDB containers:
version: '3.1'
services:
db:
image: postgres:latest
restart: always
environment:
POSTGRES_USER: ABC233
POSTGRES_PASSWORD: ABC0012
POSTGRES_DB: test
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
mongodb:
image: mongo
container_name: mongodb
restart: unless-stopped
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
volumes:
mongo-data:
driver: local
postgres_data:
Create the containers by running:
docker-compose up -d
Make sure you keep your docker desktop running.
After setting up docker if you are using rds you can run alembic to create all the tables required for your project.
Global Setup and Teardown
global-setup.ts
This file is responsible for creating a test user at the beginning of the test execution so that it can be used dynamically.
import { request } from '@playwright/test';
import fs from 'fs';
import path from 'path';
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod';
const userFilePath = path.join(__dirname, 'user-data.json');
const generateValidData = () => {
return {
first_name: 'Test',
last_name: 'User',
country_code: '+91',
phone_number: `9${Math.floor(Math.random() * 1000000000).toString().padStart(9, '0')}`,
email_address: `shrinit.poojary+${uuidv4().substring(0, 8)}@7edge.com`,
signature_photo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
extras: { deactivate_reason: '' }
};
};
async function globalSetup() {
const apiRequest = await request.newContext();
const validData = generateValidData();
const response = await apiRequest.post(`${process.env.BASE_API_URL}/sdst/create`, {
data: validData,
headers: { 'Content-Type': 'application/json' }
});
if (response.status() !== 201) {
throw new Error('Failed to create user');
}
const userData = await response.json();
fs.writeFileSync(userFilePath, JSON.stringify(userData));
}
export default globalSetup;
global-teardown.ts
This file is responsible for cleaning up test data after execution. It deletes any records created by global-setup.ts .
import { request } from '@playwright/test';
import fs from 'fs';
import path from 'path';
const userFilePath = path.join(__dirname, 'user-data.json');
async function globalTeardown() {
if (!fs.existsSync(userFilePath)) {
console.log('No user data found for deletion.');
return;
}
const userData = JSON.parse(fs.readFileSync(userFilePath, 'utf8'));
const apiRequest = await request.newContext();
const response = await apiRequest.delete(`${process.env.BASE_API_URL}/sdst-bdd`, {
data: { 'username': userData.data.phone_number },
headers: { 'Content-Type': 'application/json' }
});
if (response.status() === 200) {
console.log(`User ${userData.id} deleted successfully.`);
}
fs.unlinkSync(userFilePath);
}
export default globalTeardown;
Exporting Environment Variables
Ensure all required environment variables are exported, including AWS credentials for authentication.
Step 3: Folder Structure and Test Cases
3.1 Folder Structure Overview
.
├── docker-compose.yaml
├── global-setup.ts
├── global-teardown.ts
├── mongo-test.ts
├── test-sdst
│ ├── mongo-test.spec.ts
│ └── subdistributer-create.spec.ts
└── test-sdst-registration
├── basic-details.spec.ts
├── business-details.spec.ts
└── send-otp.spec.ts
Explanation
This folder structure is designed to maintain a structured and scalable testing framework. Each folder represents a specific service, and within each service folder, test files correspond to the different APIs being tested.
docker-compose.yaml- Defines the database services (PostgreSQL and MongoDB) that run inside Docker containers for isolated testing.global-setup.ts- Creates a test user dynamically before test execution.global-teardown.ts- Cleans up the test user data after test execution.mongo-test.ts- Contains MongoDB-specific handlers for testing.test-sdst/- Contains test cases related to sub-distributor APIs.mongo-test.spec.ts- Tests MongoDB insert operations.subdistributer-create.spec.ts- Tests sub-distributor creation.
test-sdst-registration/- Contains test cases for sub-distributor registration processes.basic-details.spec.ts- Tests the basic details submission.business-details.spec.ts- Tests business details submission.send-otp.spec.ts- Tests the OTP sending functionality.
3.2 Example Test Case: send-otp.spec.ts
This test verifies the OTP sending functionality by making API calls to the otp_send handler.
File: send-otp.spec.ts
import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
// @ts-ignore
const { handler } = require('../../services/Sub_Distributor_Mobile/sdst/dist/handlers/otp_send');
const userFilePath = '../user-data.json';
const userData = JSON.parse(fs.readFileSync(userFilePath, 'utf8')).data;
test.describe('User Verification Handler Tests', () => {
test('should successfully send OTP when valid email is provided @email-otp', async () => {
const validData = {
email_address: "shrinit.poojary12+6aa28308@gmail.com",
name: 'Shrinit Poojary',
user_type: 'sdst'
};
const event = { body: JSON.stringify(validData) };
const response = await handler(event);
expect(response.statusCode).toBe(200);
const responseBody = JSON.parse(response.body);
expect(responseBody.message).toBe('OTP sent successfully');
});
test('should fail with invalid email format', async () => {
const invalidData = { email_address: 'invalid-email' };
const event = { body: JSON.stringify(invalidData) };
const response = await handler(event);
expect(response.statusCode).toBe(400);
const responseBody = JSON.parse(response.body);
expect(responseBody.message).toBe('Invalid email address format');
});
test('should fail when neither email nor phone number is provided', async () => {
const event = { body: JSON.stringify({}) };
const response = await handler(event);
expect(response.statusCode).toBe(400);
const responseBody = JSON.parse(response.body);
expect(responseBody.message).toBe('Email or Phone Number is required');
});
});
Explanation of Imports
import { test, expect } from '@playwright/test';- Imports Playwright’s testing framework.import fs from 'fs'; import path from 'path';- Imports Node.js modules for file handling.const { handler } = require('../../services/Sub_Distributor_Mobile/sdst/dist/handlers/otp_send');- Imports theotp_sendhandler that processes OTP requests.
Test Breakdown
- Valid Email OTP Test
- Sends an OTP using a valid email.
- Expects a 200 response and a success message.
- Invalid Email Format Test
- Sends a malformed email address.
- Expects a 400 response with an error message.
- Missing Email and Phone Number Test
- Sends an empty request.
- Expects a 400 response indicating missing fields.
3.3 Example Test Case: mongo-test.spec.ts
This test validates MongoDB insertion operations.
File: mongo-test.spec.ts
import { test, expect } from '@playwright/test';
// @ts-ignore
const { handler } = require('../mongo-test.ts');
test.describe('MongoDB Insert Handler Tests', () => {
test('should successfully insert item into MongoDB @insert-mongo', async () => {
const validData = { name: 'Test Item', value: 100 };
const event = { body: JSON.stringify(validData) };
const response = await handler(event);
expect(response.statusCode).toBe(201);
const responseBody = JSON.parse(response.body);
expect(responseBody.message).toBe('Item inserted');
expect(responseBody.data.name).toBe(validData.name);
expect(responseBody.data.value).toBe(validData.value);
});
test('should fail when name is missing', async () => {
const invalidData = { value: 100 };
const event = { body: JSON.stringify(invalidData) };
const response = await handler(event);
expect(response.statusCode).toBe(400);
const responseBody = JSON.parse(response.body);
expect(responseBody.message).toBe('Missing required fields: name, value');
});
});
Explanation of Imports
import { test, expect } from '@playwright/test';- Imports Playwright’s testing framework.const { handler } = require('../mongo-test.ts');- Imports the MongoDB handler for database operations.
Test Breakdown
- Successful Insert Test
- Sends valid data to the MongoDB insert function.
- Expects a 201 response and confirms data insertion.
- Missing Name Test
- Sends a request missing the
namefield. - Expects a 400 response with an error message.
Running the Tests
Run the following command in the root directory:
npm test
This will execute all test cases inside the test folder.
Here’s your cleaned test output with unnecessary debug statements removed:
> Product-backend-apis@1.0.0 test
> nyc --reporter=html --reporter=text playwright test
Running 10 tests using 2 workers
✓ 1 …n] › tests/test-sdst-registration/mongo-test.spec.ts:7:7 › MongoDB Insert Handler Tests › should successfully insert item into MongoDB @insert-mongo (115ms)
✓ 2 [test-sdst-registration] › tests/test-sdst-registration/mongo-test.spec.ts:20:7 › MongoDB Insert Handler Tests › should fail when name is missing (6ms)
✓ 3 [test-sdst-registration] › tests/test-sdst-registration/mongo-test.spec.ts:31:7 › MongoDB Insert Handler Tests › should fail when value is missing (4ms)
✓ 4 [test-sdst-registration] › tests/test-sdst-registration/mongo-test.spec.ts:42:7 › MongoDB Insert Handler Tests › should fail with invalid JSON payload (9ms)
✓ 5 …test-sdst-registration/send-otp.spec.ts:12:7 › User Verification Handler Tests › should successfully send OTP when valid email is provided @email-otp (1.4s)
✓ 6 [test-sdst-registration] › tests/test-sdst-registration/send-otp.spec.ts:26:7 › User Verification Handler Tests › should fail with invalid email format (9ms)
✓ 7 …] › tests/test-sdst-registration/send-otp.spec.ts:36:7 › User Verification Handler Tests › should fail when neither email nor phone number is provided (7ms)
✓ 8 …on] › tests/test-sdst-registration/send-otp.spec.ts:45:7 › User Verification Handler Tests › should fail when both email and phone number are provided (4ms)
✓ 9 … › tests/test-sdst-registration/send-otp.spec.ts:59:7 › User Verification Handler Tests › should fail when user does not exist in forgot password flow (7ms)
✓ 10 …ion] › tests/test-sdst-registration/send-otp.spec.ts:74:7 › User Verification Handler Tests › should successfully send OTP for a valid phone number (893ms)
10 passed (12.0s)
To open last HTML report run:
npx playwright show-report
------------------------------------------------------------------------------------|---------|----------|---------|---------|--------------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------------------------------------------------------------------|---------|----------|---------|---------|--------------------------------------
All files | 40.7 | 41.3 | 23.07 | 41.89 |
Product-Backend-APIs | 100 | 100 | 100 | 100 |
playwright.config.ts | 100 | 100 | 100 | 100 |
Product-Backend-APIs/services/Sub_Distributor_Mobile/sdst/dist/handlers | 91.37 | 77.14 | 100 | 91.37 |
otp_send.js | 91.37 | 77.14 | 100 | 91.37 | 60,99,112,159-160
Product-Backend-APIs/services/Sub_Distributor_Mobile/sdst/dist/lib | 15.97 | 0 | 0 | 16.77 |
helper.js | 15.97 | 0 | 0 | 16.77 | ...6-507,519-537,542,545-559,564-581
Product-Backend-APIs/services/Sub_Distributor_Mobile/sdst/dist/lib/aws | 46 | 60 | 44.44 | 47.42 |
rds_knex.js | 35.13 | 74.07 | 30 | 37.14 | 42-43,58-107
ses.js | 39.02 | 33.33 | 25 | 40 | 44-45,50,55-82
sns.js | 77.27 | 25 | 100 | 77.27 | 22,28,48-50
Product-Backend-APIs/services/Sub_Distributor_Mobile/sdst/dist/utils/sms-templates | 100 | 100 | 100 | 100 |
sdst_verify_otp.js | 100 | 100 | 100 | 100 |
------------------------------------------------------------------------------------|---------|----------|---------|---------|--------------------------------------
Step 4: Open the Coverage Report
- Right-click on
index.htmlin thecoveragefolder. - Select “Open with Live Server” (if using VS Code with the Live Server extension) or open it in a browser manually.
- The coverage report will display overall test coverage percentages for statements, branches, functions, and lines.
Step 5: Analyze the Code Coverage Report
The report shows:
- Fully tested files (highlighted in green)
- Partially tested files (highlighted in yellow)
- Files with poor test coverage (highlighted in red)
Click on any file to explore line-by-line test coverage.
Step 6: Improve Test Coverage
If certain files have low test coverage:
- Identify untested code blocks.
- Write additional test cases to cover them.
- Re-run
npm testand verify the updated coverage.
Open the Playwright Report
Navigate to the playwright-report directory and open index.html in a browser:

npx playwright show-report
This command launches a browser displaying the Playwright test results.
Conclusion
By following this structured approach to Test-Driven Development (TDD) with TypeScript, Playwright, and API testing, we have established a robust and scalable testing framework for our backend services.
We began by setting up mock databases using Docker to simulate real-world environments and ensure our handlers work as expected. Then, we implemented global setup and teardown scripts to create and clean up test data dynamically, ensuring isolated and repeatable tests. Finally, we structured our test suite to mirror our service architecture, allowing seamless organization and efficient test execution.
With Playwright, we not only tested APIs for correctness but also enforced strict validation through schema validation (Zod) and real-time API assertions. This ensures that every request and response meets the required standards before being deployed.
Adopting TDD brings multiple benefits:
- Faster debugging with early issue detection
- Consistent API behavior across services
- Scalability and maintainability as the application grows
- Confidence in deployments with automated test execution
By incorporating these practices into your development workflow, you can significantly improve the reliability, security, and efficiency of your backend APIs. Whether you’re working on a small service or a large-scale distributed system, this approach will help ensure seamless integration and a smooth user experience.
Now, with a well-structured and automated testing pipeline, you can ship features faster without compromising quality.



