Testing Guide
Learn how to write and run tests for your JifiJs application.
Overview
JifiJs uses Jest as the testing framework with Supertest for HTTP assertions. All tests are written in TypeScript.
Test Structure
tests/
├── setup.ts # Global test configuration
├── services/
│ ├── auth.service.test.ts # Service tests
│ ├── admin-user.service.test.ts
│ └── ...
├── middlewares/
│ └── auth.middleware.test.ts # Middleware tests
└── validations/
└── auth.validation.test.ts # Validation tests
Running Tests
# Run all tests
npm test
# Run in watch mode
npm run test:watch
# Run with coverage
npm run test:coverage
# Run specific test file
npm test -- auth.service.test.ts
# Run verbose
npm run test:verbose
Test Configuration
jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.{ts,js}',
'utils/**/*.{ts,js}',
'!**/*.d.ts',
],
testMatch: ['**/tests/**/*.test.{ts,js}'],
testTimeout: 30000,
};
Global Setup (tests/setup.ts)
import mongoose from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import cacheService from '../utils/helpers/cache.helper';
import tokenBlacklist from '../utils/bases/blackList.service';
let mongoServer: MongoMemoryServer;
// Setup: Before all tests
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
});
// Cleanup: After each test
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});
// Teardown: After all tests
afterAll(async () => {
await cacheService.shutdown();
tokenBlacklist.destroy();
await mongoose.disconnect();
await mongoServer.stop();
});
Writing Tests
Service Tests
import authService from '../../src/services/auth/auth.service';
import { faker } from '@faker-js/faker';
describe('AuthService', () => {
describe('register', () => {
it('should register a new user successfully', async () => {
const userData = {
first_name: faker.person.firstName(),
last_name: faker.person.lastName(),
email: faker.internet.email(),
password: 'SecurePassword123!',
};
const result = await authService.register(userData as any);
expect(result.error).toBe(false);
expect(result.data).toHaveProperty('_id');
expect(result.data?.email).toBe(userData.email.toLowerCase());
expect(result.data).not.toHaveProperty('password');
});
it('should not register duplicate email', async () => {
const userData = {
first_name: 'John',
last_name: 'Doe',
email: 'test@example.com',
password: 'SecurePassword123!',
};
// First registration
await authService.register(userData as any);
// Second registration with same email
const result = await authService.register(userData as any);
expect(result.error).toBe(true);
expect(result.message).toContain('already exists');
});
it('should hash password', async () => {
const userData = {
first_name: 'John',
last_name: 'Doe',
email: 'hash@example.com',
password: 'SecurePassword123!',
};
const result = await authService.register(userData as any);
const user = await authService.model.findById(result.data?._id).select('+password');
expect(user?.password).toBeDefined();
expect(user?.password).not.toBe(userData.password);
expect(user?.password).toHaveLength(60); // BCrypt hash length
});
});
describe('login', () => {
beforeEach(async () => {
// Create test user
await authService.register({
first_name: 'Test',
last_name: 'User',
email: 'login@test.com',
password: 'Password123!',
is_verified: true,
} as any);
});
it('should login with valid credentials', async () => {
const result = await authService.login(
'login@test.com',
'Password123!',
{} as any
);
expect(result.error).toBe(false);
expect(result.data).toHaveProperty('access_token');
expect(result.data).toHaveProperty('refresh_token');
});
it('should not login with invalid password', async () => {
const result = await authService.login(
'login@test.com',
'WrongPassword',
{} as any
);
expect(result.error).toBe(true);
expect(result.message).toContain('Invalid');
});
it('should not login unverified user', async () => {
await authService.register({
first_name: 'Unverified',
last_name: 'User',
email: 'unverified@test.com',
password: 'Password123!',
is_verified: false,
} as any);
const result = await authService.login(
'unverified@test.com',
'Password123!',
{} as any
);
expect(result.error).toBe(true);
expect(result.message).toContain('verify');
});
});
});
Controller Tests (with Supertest)
import request from 'supertest';
import { app } from '../../main';
import { faker } from '@faker-js/faker';
describe('Auth Controller', () => {
describe('POST /auth/register', () => {
it('should register a new user', async () => {
const userData = {
first_name: faker.person.firstName(),
last_name: faker.person.lastName(),
email: faker.internet.email(),
password: 'SecurePassword123!',
};
const response = await request(app)
.post('/auth/register')
.send(userData)
.expect(201);
expect(response.body.status).toBe('SUCCESS');
expect(response.body.data).toHaveProperty('email', userData.email.toLowerCase());
});
it('should validate required fields', async () => {
const response = await request(app)
.post('/auth/register')
.send({})
.expect(400);
expect(response.body.status).toBe('FAILED');
expect(response.body.message).toContain('required');
});
it('should validate email format', async () => {
const response = await request(app)
.post('/auth/register')
.send({
first_name: 'John',
last_name: 'Doe',
email: 'invalid-email',
password: 'Password123!',
})
.expect(400);
expect(response.body.message).toContain('email');
});
});
describe('POST /auth/login', () => {
beforeEach(async () => {
// Create and verify test user
await request(app)
.post('/auth/register')
.send({
first_name: 'Login',
last_name: 'Test',
email: 'login@test.com',
password: 'Password123!',
});
// Manually verify user for testing
const Auth = require('../../src/models/auth/auth.model').default;
await Auth.updateOne(
{ email: 'login@test.com' },
{ is_verified: true }
);
});
it('should login with valid credentials', async () => {
const response = await request(app)
.post('/auth/login')
.send({
email: 'login@test.com',
password: 'Password123!',
})
.expect(200);
expect(response.body.data).toHaveProperty('access_token');
expect(response.body.data).toHaveProperty('refresh_token');
});
it('should not login with invalid credentials', async () => {
await request(app)
.post('/auth/login')
.send({
email: 'login@test.com',
password: 'WrongPassword',
})
.expect(401);
});
});
});
Middleware Tests
import { Request, Response, NextFunction } from 'express';
import { isLogin } from '../../utils/middlewares/auth/auth.middleware';
describe('Auth Middleware', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let nextFunction: NextFunction;
beforeEach(() => {
mockRequest = {};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
nextFunction = jest.fn();
});
it('should call next() with valid token', async () => {
// Setup mock request with valid token
mockRequest.headers = {
authorization: 'Bearer valid_token_here',
};
await isLogin(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(nextFunction).toHaveBeenCalled();
});
it('should return 401 without token', async () => {
mockRequest.headers = {};
await isLogin(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(nextFunction).not.toHaveBeenCalled();
});
});
Validation Tests
import authValidation from '../../utils/validations/auth.validation';
describe('Auth Validation', () => {
describe('register', () => {
it('should validate correct data', () => {
const data = {
first_name: 'John',
last_name: 'Doe',
email: 'john@example.com',
password: 'SecurePassword123!',
};
const { error } = authValidation.register.body.validate(data);
expect(error).toBeUndefined();
});
it('should require password', () => {
const data = {
first_name: 'John',
last_name: 'Doe',
email: 'john@example.com',
};
const { error } = authValidation.register.body.validate(data);
expect(error).toBeDefined();
expect(error?.message).toContain('password');
});
it('should enforce password strength', () => {
const data = {
first_name: 'John',
last_name: 'Doe',
email: 'john@example.com',
password: 'weak',
};
const { error } = authValidation.register.body.validate(data);
expect(error).toBeDefined();
});
});
});
Testing Best Practices
1. Use Faker for Test Data
import { faker } from '@faker-js/faker';
const testUser = {
first_name: faker.person.firstName(),
last_name: faker.person.lastName(),
email: faker.internet.email(),
password: faker.internet.password({ length: 12 }),
};
2. Clean Database After Each Test
Already handled in tests/setup.ts:
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});
3. Test Edge Cases
it('should handle empty database', async () => {
const result = await service.find({});
expect(result.data).toEqual([]);
});
it('should handle invalid MongoDB ObjectId', async () => {
const result = await service.findById('invalid-id');
expect(result.error).toBe(true);
});
it('should handle network timeout', async () => {
jest.setTimeout(1000);
// Test timeout scenarios
});
4. Mock External Services
// Mock email service
jest.mock('../../utils/bases/mail.service', () => ({
sendWithQueue: jest.fn().mockResolvedValue(true),
send: jest.fn().mockResolvedValue(true),
}));
// Mock Redis
jest.mock('../../configs/redis.config', () => ({
__esModule: true,
default: () => null,
}));
5. Test Async Operations
it('should handle async operations', async () => {
await expect(
service.createUser(userData)
).resolves.toHaveProperty('_id');
await expect(
service.createUser(invalidData)
).rejects.toThrow('Validation error');
});
Coverage Goals
Aim for:
- Statements: > 80%
- Branches: > 75%
- Functions: > 80%
- Lines: > 80%
Check coverage:
npm run test:coverage
View coverage report:
open coverage/lcov-report/index.html
Continuous Integration
GitHub Actions Example
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
Troubleshooting
Tests Timeout
// Increase timeout in jest.config.js
testTimeout: 60000, // 60 seconds
// Or per test
it('long running test', async () => {
jest.setTimeout(60000);
// test code
}, 60000);
MongoDB Memory Server Issues
# Clear MongoDB binary cache
rm -rf ~/.cache/mongodb-memory-server
# Reinstall
npm install mongodb-memory-server --save-dev
Port Already in Use
Tests use the actual app server, ensure:
- Different port for tests (use
PORT=3001in.env.test) - Or don't start server in test environment
Next Steps
- Deployment - Deploy your tested application
- Best Practices - Write better code
Need help? Contact jifijs@njifanda.com