Testing Guide
This guide covers how to effectively test code that uses StrataDB.
Testing Setup
Using In-Memory Databases
The most important practice for testing is using in-memory databases to ensure tests are isolated and fast:
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import { Strata, createSchema, type Document, type Collection } from 'stratadb'
// Define your document type for testing
type TestUser = Document<{
name: string
email: string
age: number
active: boolean
}>
// Create schema for testing
const userSchema = createSchema<TestUser>()
.field('name', { type: 'TEXT', indexed: true })
.field('email', { type: 'TEXT', indexed: true, unique: true })
.field('age', { type: 'INTEGER', indexed: true })
.build()
describe('User Service', () => {
let db: Strata
let users: Collection<TestUser>
beforeEach(() => {
// Create fresh in-memory database for each test
db = new Strata({ database: ':memory:' })
users = db.collection('users', userSchema)
})
afterEach(() => {
// Close database after each test to free resources
db.close()
})
test('should create user successfully', async () => {
const result = await users.insertOne({
name: 'Alice',
email: 'alice@example.com',
age: 30,
active: true
})
expect(result.document.id).toBeDefined()
expect(result.document.name).toBe('Alice')
expect(result.document.email).toBe('alice@example.com')
expect(result.document.age).toBe(30)
})
})Test Utilities
Create reusable test utilities to reduce boilerplate in your test files:
// test-utils.ts
import { Strata } from 'stratadb'
import type { Collection } from 'stratadb'
import type { Document } from 'stratadb'
export interface TestDatabase {
db: Strata
cleanup: () => void
}
/**
* Creates a fresh in-memory database for testing
*/
export const createTestDatabase = (): TestDatabase => {
const db = new Strata({ database: ':memory:' })
return {
db,
cleanup: () => db.close()
}
}
/**
* Creates a test collection with proper cleanup
*/
export const createTestCollection = <T extends Document<unknown>>(
db: Strata,
collectionName: string,
schema: any // Your schema object
): Collection<T> => {
return db.collection(collectionName, schema)
}
/**
* Test helper that provides database and collection with automatic cleanup
*/
export const withTestDatabase = async <T>(
collectionName: string,
schema: any,
testFn: (collection: Collection<any>) => Promise<T>
): Promise<T> => {
const { db, cleanup } = createTestDatabase()
try {
const collection = createTestCollection(db, collectionName, schema)
return await testFn(collection)
} finally {
cleanup()
}
}Unit Testing
Testing Individual Operations
Test each database operation separately:
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import { Strata, createSchema } from 'stratadb'
describe('User Collection Operations', () => {
type User = Document<{ name: string; email: string }>
const userSchema = createSchema<User>()
.field('name', { type: 'TEXT', indexed: true })
.field('email', { type: 'TEXT', indexed: true, unique: true })
.build()
let db: Strata
let users: Collection<User>
beforeEach(() => {
db = new Strata({ database: ':memory:' })
users = db.collection('users', userSchema)
})
afterEach(() => db.close())
test('insertOne should create document with ID', async () => {
const result = await users.insertOne({
name: 'John Doe',
email: 'john@example.com'
})
expect(result.acknowledged).toBe(true)
expect(result.document.id).toBeDefined()
expect(result.document.name).toBe('John Doe')
expect(result.document.email).toBe('john@example.com')
})
test('findById should return document or null', async () => {
const inserted = await users.insertOne({
name: 'Jane Doe',
email: 'jane@example.com'
})
// Test finding existing document
const found = await users.findById(inserted.document.id)
expect(found).toBeDefined()
expect(found?.name).toBe('Jane Doe')
// Test finding non-existent document
const notFound = await users.findById('non-existent')
expect(notFound).toBeNull()
})
test('find should return documents matching criteria', async () => {
await users.insertMany([
{ name: 'Alice', email: 'alice@example.com', age: 25 },
{ name: 'Bob', email: 'bob@example.com', age: 30 },
{ name: 'Charlie', email: 'charlie@example.com', age: 35 }
])
const adults = await users.find({ age: { $gte: 30 } })
expect(adults).toHaveLength(2)
expect(adults.map(u => u.name)).toContain('Bob')
expect(adults.map(u => u.name)).toContain('Charlie')
})
test('updateOne should modify existing document', async () => {
const inserted = await users.insertOne({
name: 'Original Name',
email: 'test@example.com',
age: 25
})
const updated = await users.updateOne(inserted.document.id, {
name: 'Updated Name',
age: 26
})
expect(updated.modifiedCount).toBe(1)
const check = await users.findById(inserted.document.id)
expect(check?.name).toBe('Updated Name')
expect(check?.age).toBe(26)
})
test('deleteOne should remove document', async () => {
const inserted = await users.insertOne({
name: 'To Delete',
email: 'delete@example.com'
})
const deleted = await users.deleteOne(inserted.document.id)
expect(deleted).toBe(true)
const check = await users.findById(inserted.document.id)
expect(check).toBeNull()
})
})Integration Testing
Testing Business Logic
Test your application's business logic that uses StrataDB:
// user-service.ts
type User = Document<{ name: string; email: string; active: boolean }>
export class UserService {
constructor(private users: Collection<User>) {}
async createUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>) {
// Check if user already exists
const existing = await this.users.findOne({ email: userData.email })
if (existing) {
throw new Error('User already exists')
}
return await this.users.insertOne(userData)
}
async activateUser(userId: string) {
const user = await this.users.findById(userId)
if (!user) {
throw new Error('User not found')
}
if (user.active) {
throw new Error('User already active')
}
await this.users.updateOne(userId, { active: true })
return await this.users.findById(userId)
}
async getActiveUsers() {
return await this.users.find({ active: true })
}
}
// user-service.test.ts
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import { UserService } from './user-service'
import { Strata, createSchema } from 'stratadb'
describe('UserService', () => {
type User = Document<{ name: string; email: string; active: boolean }>
const userSchema = createSchema<User>()
.field('email', { type: 'TEXT', indexed: true, unique: true })
.field('active', { type: 'INTEGER', indexed: true }) // INTEGER for boolean in SQLite
.build()
let db: Strata
let users: Collection<User>
let userService: UserService
beforeEach(() => {
db = new Strata({ database: ':memory:' })
users = db.collection('users', userSchema)
userService = new UserService(users)
})
afterEach(() => db.close())
test('createUser should create new user', async () => {
const result = await userService.createUser({
name: 'New User',
email: 'newuser@example.com',
active: false
})
expect(result.document.id).toBeDefined()
expect(result.document.name).toBe('New User')
expect(result.document.email).toBe('newuser@example.com')
})
test('createUser should throw error for duplicate email', async () => {
await userService.createUser({
name: 'First User',
email: 'unique@example.com',
active: false
})
await expect(async () => {
await userService.createUser({
name: 'Second User',
email: 'unique@example.com', // Same email
active: false
})
}).rejects.toThrow('User already exists')
})
test('activateUser should activate inactive user', async () => {
const user = await userService.createUser({
name: 'Inactive User',
email: 'inactive@example.com',
active: false
})
const activated = await userService.activateUser(user.document.id)
expect(activated?.active).toBe(true)
})
test('activateUser should throw error for non-existent user', async () => {
await expect(async () => {
await userService.activateUser('non-existent-id')
}).rejects.toThrow('User not found')
})
test('getActiveUsers should return only active users', async () => {
await userService.createUser({
name: 'Active User 1',
email: 'active1@example.com',
active: true
})
await userService.createUser({
name: 'Active User 2',
email: 'active2@example.com',
active: true
})
await userService.createUser({
name: 'Inactive User',
email: 'inactive@example.com',
active: false
})
const activeUsers = await userService.getActiveUsers()
expect(activeUsers).toHaveLength(2)
expect(activeUsers.every(u => u.active)).toBe(true)
})
})Testing Validation
Testing Schema Validation
Test how your validation logic works:
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import { Strata, createSchema, wrapStandardSchema } from 'stratadb'
import { z } from 'zod'
describe('Validation Testing', () => {
type User = Document<{ name: string; email: string; age: number }>
// Create a Zod schema for validation
const ZodUser = z.object({
id: z.string(),
name: z.string().min(1).max(50),
email: z.string().email(),
age: z.number().int().min(0).max(150),
createdAt: z.number(),
updatedAt: z.number()
})
const userSchema = createSchema<User>()
.field('name', { type: 'TEXT', indexed: true })
.field('email', { type: 'TEXT', indexed: true, unique: true })
.validate(wrapStandardSchema<User>(ZodUser))
.build()
let db: Strata
let users: Collection<User>
beforeEach(() => {
db = new Strata({ database: ':memory:' })
users = db.collection('users', userSchema)
})
afterEach(() => db.close())
test('should accept valid documents', async () => {
const result = await users.insertOne({
name: 'Valid User',
email: 'valid@example.com',
age: 30
})
expect(result.document.id).toBeDefined()
})
test('should reject invalid email format', async () => {
await expect(async () => {
await users.insertOne({
name: 'Invalid Email User',
email: 'not-an-email', // Invalid email
age: 30
})
}).rejects.toThrow() // Should throw validation error
})
test('should reject invalid age', async () => {
await expect(async () => {
await users.insertOne({
name: 'Invalid Age User',
email: 'valid2@example.com',
age: -5 // Invalid age
})
}).rejects.toThrow() // Should throw validation error
})
test('should validate with custom validation function', () => {
// Test validation in isolation
if (userSchema.validate) {
const validDoc = {
id: 'test-id',
name: 'Valid Name',
email: 'valid@example.com',
age: 30,
createdAt: 123,
updatedAt: 123
}
const invalidDoc = {
id: 'test-id',
name: '', // Invalid - empty name
email: 'valid@example.com',
age: 30,
createdAt: 123,
updatedAt: 123
}
expect(userSchema.validate(validDoc)).toBe(true)
expect(userSchema.validate(invalidDoc)).toBe(false)
}
})
})Mocking and Stubbing
Creating Database Mocks
Sometimes you may want to mock the database for unit tests of dependent code:
import type { Collection } from 'stratadb'
import type { Document } from 'stratadb'
// Mock collection for unit testing without database
export const createMockCollection = <T extends Document<unknown>>(): Collection<T> => {
const items: T[] = []
return {
name: 'mock-collection',
schema: {} as any,
// Mock insertOne
insertOne: async (doc: any) => {
const newDoc = {
...doc,
id: `mock-id-${Date.now()}-${Math.random()}`,
createdAt: Date.now(),
updatedAt: Date.now()
}
items.push(newDoc)
return {
acknowledged: true as const,
document: newDoc
}
},
// Mock findById
findById: async (id: string) => {
return items.find(item => item.id === id) || null
},
// Mock find
find: async (filter: any) => {
let result = [...items]
// Simple filter implementation for mocks
if (filter && typeof filter === 'object') {
for (const [key, value] of Object.entries(filter)) {
if (typeof value === 'object' && value !== null) {
// Handle operators like { $gte: 18 }
const operator = Object.keys(value)[0]
const operand = Object.values(value)[0]
if (operator === '$gte') {
result = result.filter(item => (item as any)[key] >= operand)
} else if (operator === '$lte') {
result = result.filter(item => (item as any)[key] <= operand)
}
// Add more operators as needed for your tests
} else {
// Simple equality filter
result = result.filter(item => (item as any)[key] === value)
}
}
}
return result
},
// Add other methods as needed
findOne: async (filter: any) => {
const results = await (this as Collection<T>).find(filter)
return results[0] || null
},
updateOne: async (id: string, update: any) => {
const index = items.findIndex(item => item.id === id)
if (index === -1) {
return { matchedCount: 0, modifiedCount: 0 }
}
items[index] = {
...items[index],
...update,
updatedAt: Date.now()
}
return { matchedCount: 1, modifiedCount: 1 }
},
deleteOne: async (id: string) => {
const initialLength = items.length
const newItems = items.filter(item => item.id !== id)
const deleted = initialLength > newItems.length
items.length = 0 // Clear array
items.push(...newItems) // Add back filtered items
return deleted
},
count: async (filter: any) => {
const results = await (this as Collection<T>).find(filter)
return results.length
}
} as Collection<T>
}
// Usage in tests
import { describe, test, expect } from 'bun:test'
describe('UserService with Mock Database', () => {
test('should create user with mock database', async () => {
const mockUsers = createMockCollection<User>()
const userService = new UserService(mockUsers)
const result = await userService.createUser({
name: 'Mock User',
email: 'mock@example.com',
active: true
})
expect(result.document.id).toBeDefined()
expect(result.document.name).toBe('Mock User')
})
})Performance Testing
Testing Query Performance
Add performance testing to ensure your queries remain efficient:
import { describe, test, expect } from 'bun:test'
describe('Performance Testing', () => {
test('large dataset performance', async () => {
// This is just an example - you'd implement with real database
const startTime = performance.now()
// Insert large dataset
// Perform operations
// Measure time
const endTime = performance.now()
const executionTime = endTime - startTime
// Assert performance requirements
expect(executionTime).toBeLessThan(1000) // Should complete in under 1 second
})
})Test Patterns and Anti-Patterns
Good Patterns
- Always use in-memory databases for tests
- Create fresh database for each test to ensure isolation
- Clean up database connections after tests
- Test both positive and negative cases for validation
- Use descriptive test names that indicate expected behavior
Anti-Patterns to Avoid
// ❌ Don't use shared databases between tests
let sharedDb: Strata
let sharedUsers: Collection<User>
beforeAll(() => {
sharedDb = new Strata({ database: './shared-test.db' }) // Shared state
sharedUsers = sharedDb.collection('users', userSchema)
})
// ❌ Don't forget to close databases
test('some test', async () => {
const db = new Strata({ database: ':memory:' })
const users = db.collection('users', userSchema)
// Using db and users but never closing
// This can cause resource leaks
})
// ❌ Don't test implementation details
test('should call database.insertOne', () => {
// Testing internal implementation rather than behavior
// Use integration tests instead
})Running Tests
Test Configuration
For Bun tests with StrataDB:
// In package.json or similar configuration
{
"scripts": {
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage"
}
}Parallel Testing
Bun supports parallel test execution. Since each test uses an isolated in-memory database, tests can safely run in parallel:
# Run tests in parallel (Bun default behavior)
bun test
# Or explicitly
bun test --parallelBy following these testing patterns and best practices, you can ensure your StrataDB applications are well-tested and reliable.