Documents
Testing Strategy for Database and Models
Testing Strategy for Database and Models
Type
Document
Status
Published
Created
Nov 23, 2025
Updated
Nov 23, 2025
Updated by
Dosu Bot

Testing Approach for Database Connection and Base Schema#

Overview#

Testing for the database connection and base schema features is divided into unit and integration tests. Unit tests focus on schema-level logic and methods, using isolated environments and mocks where appropriate. Integration tests validate the interaction with real databases and external services, exercising the full stack including connection management, health checks, and CRUD operations.


Unit Tests#

Unit tests for the base schema are located in tests/unit/base-schema.test.ts. These tests verify schema creation, timestamps, soft delete fields, index addition, and utility functions. They connect to a test database, clean up collections after each test, and mock dependencies as needed. For example, tests assert that the schema includes created_at and updated_at fields, that soft delete fields are added when enabled, and that query helpers like notDeleted, onlyDeleted, and withDeleted behave as expected. Methods such as softDelete and restore are tested to ensure they correctly update document state and timestamps. The tests also verify that indexes are added and that utility functions like isSoftDeleteDocument work as intended
see example.

Example: Testing soft delete and restore methods

it('should soft delete a document', async () => {
  const doc = await TestModel.create({ name: 'Test Document' });
  await doc.softDelete!();
  expect(doc.is_deleted).toBe(true);
  expect(doc.deleted_at).toBeInstanceOf(Date);
});

it('should restore a soft deleted document', async () => {
  const doc = await TestModel.create({ name: 'Test Document' });
  await doc.softDelete!();
  await doc.restore!();
  expect(doc.is_deleted).toBe(false);
  expect(doc.deleted_at).toBeUndefined();
});

Integration Tests#

Integration tests are located in tests/integration/database-connection.test.ts. These tests use real containers for MongoDB and Redis (via Testcontainers), and set up and tear down connections in beforeAll and afterAll hooks. They cover health check endpoints, CRUD operations, transaction support, and connection resilience
see example.

Health Check Integration#

Health check tests verify that /health and /health/ready endpoints return the expected status and include MongoDB connection state.

Example:

it('should return healthy status when database is connected', async () => {
  const response = await request(app).get('/health');
  expect(response.status).toBe(200);
  expect(response.body.status).toBe('healthy');
  expect(response.body.database.mongodb).toBe('connected');
});

CRUD Operations#

CRUD operations are tested by creating, querying, updating, and deleting documents using Mongoose models. Tests also verify concurrent operations and transaction support.

Example:

it('should be able to create and query documents', async () => {
  const TestModel = mongoose.model('IntegrationTest', new mongoose.Schema({ name: String, value: Number }));
  const doc = await TestModel.create({ name: 'test', value: 42 });
  expect(doc._id).toBeDefined();
  const found = await TestModel.findById(doc._id);
  expect(found?.name).toBe('test');
  await TestModel.deleteMany({});
  await mongoose.connection.deleteModel('IntegrationTest');
});

Retry Logic#

The database connection uses retry logic to handle connection failures, attempting to connect up to a configurable number of times with a delay between attempts. Integration tests simulate connection failures and verify that retries and error handling work as expected
see implementation.

Example (conceptual):

// Simulate connection failure and verify retry attempts
await expect(connectDatabase(2, 100)).rejects.toThrow('Failed to connect to MongoDB after maximum retries');

Soft Delete#

Integration tests verify that soft-deleted documents are excluded from queries by default, and that query helpers allow access to deleted or all documents.

Example:

const doc = await TestModel.create({ name: 'ToDelete' });
await doc.softDelete!();
const active = await TestModel.find().notDeleted();
expect(active).not.toContainEqual(expect.objectContaining({ name: 'ToDelete' }));
const deleted = await TestModel.find().onlyDeleted();
expect(deleted).toContainEqual(expect.objectContaining({ name: 'ToDelete' }));

Jest Configuration#

Separate Configurations#

Jest uses separate configuration files for unit and integration tests. The main config (jest.config.js) covers unit tests, while jest.integration.config.js is dedicated to integration tests. Integration tests run in a single worker (maxWorkers: 1) to avoid interference with shared resources like Redis and MongoDB, and have increased timeouts to accommodate longer-running tests
see config.

Example (jest.integration.config.js):

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  maxWorkers: 1,
  roots: ['<rootDir>/tests/integration'],
  testMatch: ['**/*.test.ts'],
  collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/*.test.ts'],
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1' },
  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
  testTimeout: 60000,
  verbose: true,
};

Test Environment Setup#

The Jest setup file (tests/setup.ts) provisions a real MongoDB container for integration tests, sets environment variables, and cleans up resources after tests
see setup.


Coverage Thresholds and Exclusions#

Coverage thresholds are enforced globally: 50% for branches, 72% for functions, and 80% for lines and statements. Specific files with lower coverage have temporary overrides. Coverage is collected from src/**/*.ts, excluding declaration files, test files, index.ts, and example files
see config.

Example (jest.config.js excerpt):

collectCoverageFrom: [
  'src/**/*.ts',
  '!src/**/*.d.ts',
  '!src/**/*.test.ts',
  '!src/**/*.spec.ts',
  '!src/index.ts',
  '!src/examples/**/*.ts',
],
coverageThreshold: {
  global: {
    branches: 50,
    functions: 72,
    lines: 80,
    statements: 80,
  },
  './src/config/index.ts': { branches: 25 },
  './src/middleware/error-handler.ts': { branches: 40 },
  './src/routes/health.ts': { branches: 50 },
},

References#