Skip to main content

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=3001 in .env.test)
  • Or don't start server in test environment

Next Steps


Need help? Contact jifijs@njifanda.com