Skip to main content

Overview

Esix provides a mock database adapter that makes testing fast and easy without requiring a real MongoDB instance. Tests run in-memory with minimal setup.

Setup

Esix uses the DB_ADAPTER environment variable to determine which database adapter to use.

Install Dependencies

The mock adapter requires the mongo-mock package:
npm install --save-dev mongo-mock
# or
yarn add --dev mongo-mock

Configure the Mock Adapter

Set the DB_ADAPTER environment variable to 'mock' in your tests:
import { describe, it, expect, beforeEach } from 'vitest'
import { BaseModel } from 'esix'

class User extends BaseModel {
  public username = ''
  public email = ''
}

describe('User Model', () => {
  beforeEach(() => {
    process.env.DB_ADAPTER = 'mock'
    process.env.DB_DATABASE = 'test'
  })

  it('creates a new user', async () => {
    const user = await User.create({
      username: 'john',
      email: 'john@example.com'
    })

    expect(user.username).toBe('john')
    expect(user.id).toBeDefined()
  })
})

Environment Variables

When using the mock adapter, configure these environment variables:
VariableDescriptionExample
DB_ADAPTERSet to 'mock' for testing'mock'
DB_DATABASEDatabase name (can be any string)'test'
For isolation between tests, use a unique database name for each test file or suite.

Testing Patterns

Basic CRUD Operations

import { describe, it, expect, beforeEach } from 'vitest'
import { BaseModel } from 'esix'

class Post extends BaseModel {
  public title = ''
  public content = ''
  public status = 'draft'
}

describe('Post CRUD', () => {
  beforeEach(() => {
    process.env.DB_ADAPTER = 'mock'
    process.env.DB_DATABASE = 'test'
  })

  it('creates a post', async () => {
    const post = await Post.create({
      title: 'My First Post',
      content: 'Hello world!'
    })

    expect(post.id).toBeDefined()
    expect(post.title).toBe('My First Post')
  })

  it('finds a post by id', async () => {
    const created = await Post.create({ title: 'Test Post' })
    const found = await Post.find(created.id)

    expect(found).not.toBeNull()
    expect(found?.title).toBe('Test Post')
  })

  it('updates a post', async () => {
    const post = await Post.create({ title: 'Draft Post' })
    
    post.status = 'published'
    await post.save()

    const updated = await Post.find(post.id)
    expect(updated?.status).toBe('published')
  })

  it('deletes a post', async () => {
    const post = await Post.create({ title: 'To Delete' })
    await post.delete()

    const found = await Post.find(post.id)
    expect(found).toBeNull()
  })
})

Testing Queries

import { describe, it, expect, beforeEach } from 'vitest'
import { BaseModel } from 'esix'

class Product extends BaseModel {
  public name = ''
  public price = 0
  public inStock = true
}

describe('Product Queries', () => {
  beforeEach(async () => {
    process.env.DB_ADAPTER = 'mock'
    process.env.DB_DATABASE = 'test'

    // Seed test data
    await Product.create({ name: 'Laptop', price: 999, inStock: true })
    await Product.create({ name: 'Mouse', price: 25, inStock: true })
    await Product.create({ name: 'Keyboard', price: 75, inStock: false })
  })

  it('filters by price', async () => {
    const affordable = await Product.where('price', '<=', 50).get()
    
    expect(affordable).toHaveLength(1)
    expect(affordable[0].name).toBe('Mouse')
  })

  it('filters by stock status', async () => {
    const inStock = await Product.where('inStock', true).get()
    
    expect(inStock).toHaveLength(2)
  })

  it('counts products', async () => {
    const count = await Product.count()
    
    expect(count).toBe(3)
  })
})

Testing Relationships

import { describe, it, expect, beforeEach } from 'vitest'
import { BaseModel } from 'esix'

class Author extends BaseModel {
  public name = ''
}

class Book extends BaseModel {
  public title = ''
  public authorId = ''
}

describe('Author-Book Relationships', () => {
  beforeEach(() => {
    process.env.DB_ADAPTER = 'mock'
    process.env.DB_DATABASE = 'test'
  })

  it('retrieves books for an author', async () => {
    const author = await Author.create({ name: 'Jane Austen' })
    
    await Book.create({ title: 'Pride and Prejudice', authorId: author.id })
    await Book.create({ title: 'Emma', authorId: author.id })

    const books = await author.hasMany(Book).get()
    
    expect(books).toHaveLength(2)
    expect(books.map(b => b.title)).toContain('Pride and Prejudice')
  })
})

Testing Aggregations

import { describe, it, expect, beforeEach } from 'vitest'
import { BaseModel } from 'esix'

class Order extends BaseModel {
  public amount = 0
  public status = 'pending'
}

describe('Order Aggregations', () => {
  beforeEach(async () => {
    process.env.DB_ADAPTER = 'mock'
    process.env.DB_DATABASE = 'test'

    await Order.create({ amount: 100, status: 'completed' })
    await Order.create({ amount: 250, status: 'completed' })
    await Order.create({ amount: 75, status: 'pending' })
  })

  it('calculates total revenue', async () => {
    const total = await Order
      .where('status', 'completed')
      .sum('amount')
    
    expect(total).toBe(350)
  })

  it('calculates average order value', async () => {
    const avg = await Order.average('amount')
    
    expect(avg).toBeCloseTo(141.67, 2)
  })
})

Test Isolation

For better test isolation, use a unique database name for each test:
import { v4 as uuid } from 'uuid'
import { describe, it, beforeEach, afterAll } from 'vitest'
import { connectionHandler } from 'esix'

describe('Isolated Tests', () => {
  beforeEach(() => {
    process.env.DB_ADAPTER = 'mock'
    process.env.DB_DATABASE = `test-${uuid()}` // Unique DB per test
  })

  afterAll(() => {
    connectionHandler.closeConnections()
  })

  // Your tests here
})

Testing Security

Test that your application properly prevents NoSQL injection:
import { describe, it, expect, beforeEach } from 'vitest'
import { BaseModel } from 'esix'

class User extends BaseModel {
  public username = ''
  public password = ''
}

describe('NoSQL Injection Prevention', () => {
  beforeEach(async () => {
    process.env.DB_ADAPTER = 'mock'
    process.env.DB_DATABASE = 'test'

    await User.create({
      username: 'john',
      password: 'secret123'
    })
  })

  it('prevents NoSQL injection via object injection', async () => {
    const maliciousInput = { $ne: null }
    
    const user = await User
      .where('username', 'john')
      .where('password', maliciousInput)
      .first()
    
    // Should not find user because the malicious operator is sanitized
    expect(user).toBeNull()
  })

  it('safely handles valid queries', async () => {
    const user = await User
      .where('username', 'john')
      .where('password', 'secret123')
      .first()
    
    expect(user).not.toBeNull()
    expect(user?.username).toBe('john')
  })
})

Advanced: Custom Test Utilities

Create helper functions to reduce boilerplate:
import { v4 as uuid } from 'uuid'
import { connectionHandler } from 'esix'

export function setupTestDb() {
  const dbName = `test-${uuid()}`
  
  process.env.DB_ADAPTER = 'mock'
  process.env.DB_DATABASE = dbName
  
  return {
    dbName,
    cleanup: () => connectionHandler.closeConnections()
  }
}

// Usage in tests
describe('My Tests', () => {
  const db = setupTestDb()
  
  afterAll(() => db.cleanup())
  
  it('works', async () => {
    // Your test
  })
})

Testing Frameworks

Esix works with any JavaScript testing framework:
import { describe, it, expect, beforeEach } from 'vitest'

Jest

import { describe, it, expect, beforeEach } from '@jest/globals'

Mocha

import { describe, it } from 'mocha'
import { expect } from 'chai'

Performance Tips

The mock adapter is extremely fast since it runs in-memory. However, you can further improve test performance by:
  • Reusing database connections when possible
  • Minimizing the amount of test data created
  • Running tests in parallel
  • Closing connections in afterAll() hooks

Common Issues

”DB_ADAPTER not set”

Make sure you’re setting the environment variable before importing or using models:
// ❌ Wrong order
import { User } from './models'
process.env.DB_ADAPTER = 'mock'

// ✅ Correct order
process.env.DB_ADAPTER = 'mock'
import { User } from './models'

Tests Interfering with Each Other

Use unique database names per test or test suite:
beforeEach(() => {
  process.env.DB_DATABASE = `test-${Date.now()}-${Math.random()}`
})

Real-World Example

Here’s a complete example from the Esix codebase (injection.spec.ts):
import { v1 as createUuid } from 'uuid'
import { afterAll, beforeEach, describe, expect, it } from 'vitest'
import { BaseModel } from './'
import { connectionHandler } from './connection-handler'

class User extends BaseModel {
  public password = ''
  public username = ''
}

describe('Injections', () => {
  beforeEach(() => {
    Object.assign(process.env, {
      DB_ADAPTER: 'mock',
      DB_DATABASE: `test-${createUuid()}`
    })
  })

  afterAll(() => {
    connectionHandler.closeConnections()
  })

  it('prevents NoSQL injections using an object', async () => {
    await User.create({
      password: 'secretstuff',
      username: 'Tim'
    })

    const user = await User
      .where('username', 'Tim')
      .where('password', { $ne: 1 })
      .first()

    expect(user).toBeNull()
  })
})

Next Steps