- Backend Testing
- When to use this skill
- Specific situations that should trigger this skill:
- New feature development
-
- Write tests first using TDD (Test-Driven Development)
- Adding API endpoints
-
- Test success and failure cases for REST APIs
- Bug fixes
-
- Add tests to prevent regressions
- Before refactoring
-
- Write tests that guarantee existing behavior
- CI/CD setup
-
- Build automated test pipelines
- Input Format
- Format and required/optional information to collect from the user:
- Required information
- Framework
-
- Express, Django, FastAPI, Spring Boot, etc.
- Test tool
-
- Jest, Pytest, Mocha/Chai, JUnit, etc.
- Test target
-
- API endpoints, business logic, DB operations, etc.
- Optional information
- Database
-
- PostgreSQL, MySQL, MongoDB (default: in-memory DB)
- Mocking library
-
- jest.mock, sinon, unittest.mock (default: framework built-in)
- Coverage target
-
- 80%, 90%, etc. (default: 80%)
- E2E tool
- Supertest, TestClient, RestAssured (optional)
Input example
Test the user authentication endpoints for an Express.js API:
- Framework: Express + TypeScript
- Test tool: Jest + Supertest
- Target: POST /auth/register, POST /auth/login
- DB: PostgreSQL (in-memory for tests)
- Coverage: 90% or above
Instructions
Step-by-step task order to follow precisely.
Step 1: Set up the test environment
Install and configure the test framework and tools.
Tasks
:
Install test libraries
Configure test database (in-memory or separate DB)
Separate environment variables (.env.test)
Configure jest.config.js or pytest.ini
Example
(Node.js + Jest + Supertest):
npm
install
--save-dev jest ts-jest @types/jest supertest @types/supertest
jest.config.js
:
module
.
exports
=
{
preset
:
'ts-jest'
,
testEnvironment
:
'node'
,
roots
:
[
'
/src' ] , testMatch : [ '/tests//.test.ts' ] , collectCoverageFrom : [ 'src//.ts' , '!src//*.d.ts' , '!src/tests/' ] , coverageThreshold : { global : { branches : 80 , functions : 80 , lines : 80 , statements : 80 } } , setupFilesAfterEnv : [ ' /src/tests/setup.ts' ] } ; setup.ts (global test configuration): import { db } from '../database' ; // Reset DB before each test beforeEach ( async ( ) => { await db . migrate . latest ( ) ; await db . seed . run ( ) ; } ) ; // Clean up after each test afterEach ( async ( ) => { await db . migrate . rollback ( ) ; } ) ; // Close connection after all tests complete afterAll ( async ( ) => { await db . destroy ( ) ; } ) ; Step 2: Write Unit Tests (business logic) Write unit tests for individual functions and classes. Tasks : Test pure functions (no dependencies) Isolate dependencies via mocking Test edge cases (boundary values, exceptions) AAA pattern (Arrange-Act-Assert) Decision criteria : No external dependencies (DB, API) -> pure Unit Test External dependencies present -> use Mock/Stub Complex logic -> test various input cases Example (password validation function): // src/utils/password.ts export function validatePassword ( password : string ) : { valid : boolean ; errors : string [ ] } { const errors : string [ ] = [ ] ; if ( password . length < 8 ) { errors . push ( 'Password must be at least 8 characters' ) ; } if ( ! / [ A - Z ] / . test ( password ) ) { errors . push ( 'Password must contain uppercase letter' ) ; } if ( ! / [ a - z ] / . test ( password ) ) { errors . push ( 'Password must contain lowercase letter' ) ; } if ( ! / \d / . test ( password ) ) { errors . push ( 'Password must contain number' ) ; } if ( ! / [ !@#$%^&* ] / . test ( password ) ) { errors . push ( 'Password must contain special character' ) ; } return { valid : errors . length === 0 , errors } ; } // src/tests/utils/password.test.ts import { validatePassword } from '../../utils/password' ; describe ( 'validatePassword' , ( ) => { it ( 'should accept valid password' , ( ) => { const result = validatePassword ( 'Password123!' ) ; expect ( result . valid ) . toBe ( true ) ; expect ( result . errors ) . toHaveLength ( 0 ) ; } ) ; it ( 'should reject password shorter than 8 characters' , ( ) => { const result = validatePassword ( 'Pass1!' ) ; expect ( result . valid ) . toBe ( false ) ; expect ( result . errors ) . toContain ( 'Password must be at least 8 characters' ) ; } ) ; it ( 'should reject password without uppercase' , ( ) => { const result = validatePassword ( 'password123!' ) ; expect ( result . valid ) . toBe ( false ) ; expect ( result . errors ) . toContain ( 'Password must contain uppercase letter' ) ; } ) ; it ( 'should reject password without lowercase' , ( ) => { const result = validatePassword ( 'PASSWORD123!' ) ; expect ( result . valid ) . toBe ( false ) ; expect ( result . errors ) . toContain ( 'Password must contain lowercase letter' ) ; } ) ; it ( 'should reject password without number' , ( ) => { const result = validatePassword ( 'Password!' ) ; expect ( result . valid ) . toBe ( false ) ; expect ( result . errors ) . toContain ( 'Password must contain number' ) ; } ) ; it ( 'should reject password without special character' , ( ) => { const result = validatePassword ( 'Password123' ) ; expect ( result . valid ) . toBe ( false ) ; expect ( result . errors ) . toContain ( 'Password must contain special character' ) ; } ) ; it ( 'should return multiple errors for invalid password' , ( ) => { const result = validatePassword ( 'pass' ) ; expect ( result . valid ) . toBe ( false ) ; expect ( result . errors . length ) . toBeGreaterThan ( 1 ) ; } ) ; } ) ; Step 3: Integration Test (API endpoints) Write integration tests for API endpoints. Tasks : Test HTTP requests/responses Success cases (200, 201) Failure cases (400, 401, 404, 500) Authentication/authorization tests Input validation tests Checklist : Verify status code Validate response body structure Confirm database state changes Validate error messages Example (Express.js + Supertest): // src/tests/api/auth.test.ts import request from 'supertest' ; import app from '../../app' ; import { db } from '../../database' ; describe ( 'POST /auth/register' , ( ) => { it ( 'should register new user successfully' , async ( ) => { const response = await request ( app ) . post ( '/api/auth/register' ) . send ( { email : 'test@example.com' , username : 'testuser' , password : 'Password123!' } ) ; expect ( response . status ) . toBe ( 201 ) ; expect ( response . body ) . toHaveProperty ( 'user' ) ; expect ( response . body ) . toHaveProperty ( 'accessToken' ) ; expect ( response . body . user . email ) . toBe ( 'test@example.com' ) ; // Verify the record was actually saved to DB const user = await db . user . findUnique ( { where : { email : 'test@example.com' } } ) ; expect ( user ) . toBeTruthy ( ) ; expect ( user . username ) . toBe ( 'testuser' ) ; } ) ; it ( 'should reject duplicate email' , async ( ) => { // Create first user await request ( app ) . post ( '/api/auth/register' ) . send ( { email : 'test@example.com' , username : 'user1' , password : 'Password123!' } ) ; // Second attempt with same email const response = await request ( app ) . post ( '/api/auth/register' ) . send ( { email : 'test@example.com' , username : 'user2' , password : 'Password123!' } ) ; expect ( response . status ) . toBe ( 409 ) ; expect ( response . body . error ) . toContain ( 'already exists' ) ; } ) ; it ( 'should reject weak password' , async ( ) => { const response = await request ( app ) . post ( '/api/auth/register' ) . send ( { email : 'test@example.com' , username : 'testuser' , password : 'weak' } ) ; expect ( response . status ) . toBe ( 400 ) ; expect ( response . body . error ) . toBeDefined ( ) ; } ) ; it ( 'should reject missing fields' , async ( ) => { const response = await request ( app ) . post ( '/api/auth/register' ) . send ( { email : 'test@example.com' // username, password omitted } ) ; expect ( response . status ) . toBe ( 400 ) ; } ) ; } ) ; describe ( 'POST /auth/login' , ( ) => { beforeEach ( async ( ) => { // Create test user await request ( app ) . post ( '/api/auth/register' ) . send ( { email : 'test@example.com' , username : 'testuser' , password : 'Password123!' } ) ; } ) ; it ( 'should login with valid credentials' , async ( ) => { const response = await request ( app ) . post ( '/api/auth/login' ) . send ( { email : 'test@example.com' , password : 'Password123!' } ) ; expect ( response . status ) . toBe ( 200 ) ; expect ( response . body ) . toHaveProperty ( 'accessToken' ) ; expect ( response . body ) . toHaveProperty ( 'refreshToken' ) ; expect ( response . body . user . email ) . toBe ( 'test@example.com' ) ; } ) ; it ( 'should reject invalid password' , async ( ) => { const response = await request ( app ) . post ( '/api/auth/login' ) . send ( { email : 'test@example.com' , password : 'WrongPassword123!' } ) ; expect ( response . status ) . toBe ( 401 ) ; expect ( response . body . error ) . toContain ( 'Invalid credentials' ) ; } ) ; it ( 'should reject non-existent user' , async ( ) => { const response = await request ( app ) . post ( '/api/auth/login' ) . send ( { email : 'nonexistent@example.com' , password : 'Password123!' } ) ; expect ( response . status ) . toBe ( 401 ) ; } ) ; } ) ; Step 4: Authentication/Authorization Tests Test JWT tokens and role-based access control. Tasks : Confirm 401 when accessing without a token Confirm successful access with a valid token Test expired token handling Role-based permission tests Example : describe ( 'Protected Routes' , ( ) => { let accessToken : string ; let adminToken : string ; beforeEach ( async ( ) => { // Regular user token const userResponse = await request ( app ) . post ( '/api/auth/register' ) . send ( { email : 'user@example.com' , username : 'user' , password : 'Password123!' } ) ; accessToken = userResponse . body . accessToken ; // Admin token const adminResponse = await request ( app ) . post ( '/api/auth/register' ) . send ( { email : 'admin@example.com' , username : 'admin' , password : 'Password123!' } ) ; // Update role to 'admin' in DB await db . user . update ( { where : { email : 'admin@example.com' } , data : { role : 'admin' } } ) ; // Log in again to get a new token const loginResponse = await request ( app ) . post ( '/api/auth/login' ) . send ( { email : 'admin@example.com' , password : 'Password123!' } ) ; adminToken = loginResponse . body . accessToken ; } ) ; describe ( 'GET /api/auth/me' , ( ) => { it ( 'should return current user with valid token' , async ( ) => { const response = await request ( app ) . get ( '/api/auth/me' ) . set ( 'Authorization' , Bearer ${ accessToken }) ; expect ( response . status ) . toBe ( 200 ) ; expect ( response . body . user . email ) . toBe ( 'user@example.com' ) ; } ) ; it ( 'should reject request without token' , async ( ) => { const response = await request ( app ) . get ( '/api/auth/me' ) ; expect ( response . status ) . toBe ( 401 ) ; } ) ; it ( 'should reject request with invalid token' , async ( ) => { const response = await request ( app ) . get ( '/api/auth/me' ) . set ( 'Authorization' , 'Bearer invalid-token' ) ; expect ( response . status ) . toBe ( 403 ) ; } ) ; } ) ; describe ( 'DELETE /api/users/:id (Admin only)' , ( ) => { it ( 'should allow admin to delete user' , async ( ) => { const targetUser = await db . user . findUnique ( { where : { email : 'user@example.com' } } ) ; const response = await request ( app ) . delete (/api/users/ ${ targetUser . id }) . set ( 'Authorization' ,Bearer ${ adminToken }) ; expect ( response . status ) . toBe ( 200 ) ; } ) ; it ( 'should forbid non-admin from deleting user' , async ( ) => { const targetUser = await db . user . findUnique ( { where : { email : 'user@example.com' } } ) ; const response = await request ( app ) . delete (/api/users/ ${ targetUser . id }) . set ( 'Authorization' ,Bearer ${ accessToken }) ; expect ( response . status ) . toBe ( 403 ) ; } ) ; } ) ; } ) ; Step 5: Mocking and Test Isolation Mock external dependencies to isolate tests. Tasks : Mock external APIs Mock email sending Mock file system Mock time-related functions Example (mocking an external API): // src/services/emailService.ts export async function sendVerificationEmail ( email : string , token : string ) : Promise < void{ const response = await fetch ( 'https://api.sendgrid.com/v3/mail/send' , { method : 'POST' , headers : { 'Authorization' :
Bearer ${ process . env . SENDGRID_API_KEY }} , body : JSON . stringify ( { to : email , subject : 'Verify your email' , html :<a href="https://example.com/verify?token= ${ token } ">Verify</a>} ) } ) ; if ( ! response . ok ) { throw new Error ( 'Failed to send email' ) ; } } // src/tests/services/emailService.test.ts import { sendVerificationEmail } from '../../services/emailService' ; // Mock fetch global . fetch = jest . fn ( ) ; describe ( 'sendVerificationEmail' , ( ) => { beforeEach ( ( ) => { ( fetch as jest . Mock ) . mockClear ( ) ; } ) ; it ( 'should send email successfully' , async ( ) => { ( fetch as jest . Mock ) . mockResolvedValueOnce ( { ok : true , status : 200 } ) ; await expect ( sendVerificationEmail ( 'test@example.com' , 'token123' ) ) . resolves . toBeUndefined ( ) ; expect ( fetch ) . toHaveBeenCalledWith ( 'https://api.sendgrid.com/v3/mail/send' , expect . objectContaining ( { method : 'POST' } ) ) ; } ) ; it ( 'should throw error if email sending fails' , async ( ) => { ( fetch as jest . Mock ) . mockResolvedValueOnce ( { ok : false , status : 500 } ) ; await expect ( sendVerificationEmail ( 'test@example.com' , 'token123' ) ) . rejects . toThrow ( 'Failed to send email' ) ; } ) ; } ) ; Output format Defines the exact format that outputs must follow. Basic structure project/ ├── src/ │ ├── tests/ │ │ ├── setup.ts # Global test configuration │ │ ├── utils/ │ │ │ └── password.test.ts # Unit tests │ │ ├── services/ │ │ │ └── emailService.test.ts │ │ └── api/ │ │ ├── auth.test.ts # Integration tests │ │ └── users.test.ts │ └── ... ├── jest.config.js └── package.json Test run scripts (package.json) { "scripts" : { "test" : "jest" , "test:watch" : "jest --watch" , "test:coverage" : "jest --coverage" , "test:ci" : "jest --ci --coverage --maxWorkers=2" } } Coverage report $ npm run test:coverage
|
|
|
|
| File | % Stmts | % Branch | % Funcs | % Lines |
|
|
|
|
| All files | 92.5 | 88.3 | 95.2 | 92.8 | auth/ | 95.0 | 90.0 | 100.0 | 95.0 | middleware.ts | 95.0 | 90.0 | 100.0 | 95.0 | routes.ts | 95.0 | 90.0 | 100.0 | 95.0 | utils/ | 90.0 | 85.0 | 90.0 | 90.0 | password.ts | 90.0 | 85.0 | 90.0 | 90.0 |
|
|
|
|
- |
- Constraints
- Rules and prohibitions that must be strictly followed.
- Required rules (MUST)
- Test isolation
-
- Each test must be runnable independently
- Reset state with beforeEach/afterEach
- Do not depend on test execution order
- Clear test names
-
- The name must convey what the test verifies
- ✅ 'should reject duplicate email'
- ❌ 'test1'
- AAA pattern
-
- Arrange (setup) - Act (execute) - Assert (verify) structure
- Improves readability
- Clarifies test intent
- Prohibited (MUST NOT)
- No production DB
-
- Tests must use a separate or in-memory DB
- Risk of losing real data
- Cannot isolate tests
- No real external API calls
-
- Mock all external services
- Removes network dependency
- Speeds up tests
- Reduces costs
- No Sleep/Timeout abuse
-
- Use fake timers for time-based tests
- jest.useFakeTimers()
- Prevents test slowdowns
- Security rules
- No hardcoded secrets
-
- Never hardcode API keys or passwords in test code
- Separate environment variables
-
- Use .env.test file
- Examples
- Example 1: Python FastAPI tests (Pytest)
- Situation
- Testing a FastAPI REST API User request : Test the user API built with FastAPI using pytest. Final result :
tests/conftest.py
import pytest from fastapi . testclient import TestClient from sqlalchemy import create_engine from sqlalchemy . orm import sessionmaker from app . main import app from app . database import Base , get_db
In-memory SQLite for tests
SQLALCHEMY_DATABASE_URL
"sqlite:///./test.db" engine = create_engine ( SQLALCHEMY_DATABASE_URL , connect_args = { "check_same_thread" : False } ) TestingSessionLocal = sessionmaker ( autocommit = False , autoflush = False , bind = engine ) @pytest . fixture ( scope = "function" ) def db_session ( ) : Base . metadata . create_all ( bind = engine ) db = TestingSessionLocal ( ) try : yield db finally : db . close ( ) Base . metadata . drop_all ( bind = engine ) @pytest . fixture ( scope = "function" ) def client ( db_session ) : def override_get_db ( ) : try : yield db_session finally : db_session . close ( ) app . dependency_overrides [ get_db ] = override_get_db yield TestClient ( app ) app . dependency_overrides . clear ( )
tests/test_auth.py
def test_register_user_success ( client ) : response = client . post ( "/auth/register" , json = { "email" : "test@example.com" , "username" : "testuser" , "password" : "Password123!" } ) assert response . status_code == 201 assert "access_token" in response . json ( ) assert response . json ( ) [ "user" ] [ "email" ] == "test@example.com" def test_register_duplicate_email ( client ) :
First user
client . post ( "/auth/register" , json = { "email" : "test@example.com" , "username" : "user1" , "password" : "Password123!" } )
Duplicate email
response
client . post ( "/auth/register" , json = { "email" : "test@example.com" , "username" : "user2" , "password" : "Password123!" } ) assert response . status_code == 409 assert "already exists" in response . json ( ) [ "detail" ] def test_login_success ( client ) :
Register
client . post ( "/auth/register" , json = { "email" : "test@example.com" , "username" : "testuser" , "password" : "Password123!" } )
Login
response
client . post ( "/auth/login" , json = { "email" : "test@example.com" , "password" : "Password123!" } ) assert response . status_code == 200 assert "access_token" in response . json ( ) def test_protected_route_without_token ( client ) : response = client . get ( "/auth/me" ) assert response . status_code == 401 def test_protected_route_with_token ( client ) :
Register and get token
register_response
client . post ( "/auth/register" , json = { "email" : "test@example.com" , "username" : "testuser" , "password" : "Password123!" } ) token = register_response . json ( ) [ "access_token" ]
Access protected route
response
- client
- .
- get
- (
- "/auth/me"
- ,
- headers
- =
- {
- "Authorization"
- :
- f"Bearer
- {
- token
- }
- "
- }
- )
- assert
- response
- .
- status_code
- ==
- 200
- assert
- response
- .
- json
- (
- )
- [
- "email"
- ]
- ==
- "test@example.com"
- Best practices
- Quality improvements
- TDD (Test-Driven Development)
-
- Write tests before writing code
- Clarifies requirements
- Improves design
- Naturally achieves high coverage
- Given-When-Then pattern
-
- Write tests in BDD style
- it
- (
- 'should return 404 when user not found'
- ,
- async
- (
- )
- =>
- {
- // Given: a non-existent user ID
- const
- nonExistentId
- =
- 'non-existent-uuid'
- ;
- // When: attempting to look up that user
- const
- response
- =
- await
- request
- (
- app
- )
- .
- get
- (
- `
- /users/
- ${
- nonExistentId
- }
- `
- )
- ;
- // Then: 404 response
- expect
- (
- response
- .
- status
- )
- .
- toBe
- (
- 404
- )
- ;
- }
- )
- ;
- Test Fixtures
-
- Reusable test data
- const
- validUser
- =
- {
- :
- 'test@example.com'
- ,
- username
- :
- 'testuser'
- ,
- password
- :
- 'Password123!'
- }
- ;
- Efficiency improvements
- Parallel execution
-
- Speed up tests with Jest's
- --maxWorkers
- option
- Snapshot Testing
-
- Save snapshots of UI components or JSON responses
- Coverage thresholds
-
- Enforce minimum coverage in jest.config.js
- Common Issues
- Issue 1: Test failures caused by shared state between tests
- Symptom
-
- Passes individually but fails when run together
- Cause
-
- DB state shared due to missing beforeEach/afterEach
- Fix
- :
- beforeEach
- (
- async
- (
- )
- =>
- {
- await
- db
- .
- migrate
- .
- rollback
- (
- )
- ;
- await
- db
- .
- migrate
- .
- latest
- (
- )
- ;
- }
- )
- ;
- Issue 2: "Jest did not exit one second after the test run"
- Symptom
-
- Process does not exit after tests complete
- Cause
-
- DB connections, servers, etc. not cleaned up
- Fix
- :
- afterAll
- (
- async
- (
- )
- =>
- {
- await
- db
- .
- destroy
- (
- )
- ;
- await
- server
- .
- close
- (
- )
- ;
- }
- )
- ;
- Issue 3: Async test timeout
- Symptom
-
- "Timeout - Async callback was not invoked"
- Cause
-
- Missing async/await or unhandled Promise
- Fix
- :
- // Bad
- it
- (
- 'should work'
- ,
- (
- )
- =>
- {
- request
- (
- app
- )
- .
- get
- (
- '/users'
- )
- ;
- // Promise not handled
- }
- )
- ;
- // Good
- it
- (
- 'should work'
- ,
- async
- (
- )
- =>
- {
- await
- request
- (
- app
- )
- .
- get
- (
- '/users'
- )
- ;
- }
- )
- ;
- References
- Official docs
- Jest Documentation
- Pytest Documentation
- Supertest GitHub
- Learning resources
- Testing JavaScript with Kent C. Dodds
- Test-Driven Development by Example (Kent Beck)
- Tools
- Istanbul/nyc
- - code coverage
- nock
- - HTTP mocking
- faker.js
- - test data generation
- Metadata
- Version
- Current version
-
- 1.0.0
- Last updated
-
- 2025-01-01
- Compatible platforms
- Claude, ChatGPT, Gemini