Testing
Strategies and utilities for testing Kysera applications.
Transaction-Based Testing
The fastest approach - each test runs in a transaction that automatically rolls back.
import { testInTransaction } from '@kysera/testing'
describe('User Repository', () => {
it('should create user', async () => {
await testInTransaction(db, async (trx) => {
const repos = createRepos(trx)
const user = await repos.users.create({
email: 'test@example.com',
name: 'Test User'
})
expect(user.id).toBeDefined()
expect(user.email).toBe('test@example.com')
// No cleanup needed - transaction rolls back!
})
})
it('should find user by ID', async () => {
await testInTransaction(db, async (trx) => {
const repos = createRepos(trx)
const created = await repos.users.create({ ... })
const found = await repos.users.findById(created.id)
expect(found).toEqual(created)
})
})
})
Test Data Factories
Create consistent test data with factories:
import { createFactory } from '@kysera/testing'
const userFactory = createFactory({
email: (i) => `user${i}@example.com`,
name: (i) => `User ${i}`,
status: 'active'
})
// Generate unique users
const user1 = userFactory() // { email: 'user1@...', name: 'User 1', ... }
const user2 = userFactory() // { email: 'user2@...', name: 'User 2', ... }
// Override specific fields
const admin = userFactory({ status: 'admin' })
// Generate multiple
const users = Array.from({ length: 10 }, () => userFactory())
Testing Services
Test services with dependency injection:
class UserService {
constructor(private repos = createRepos(db)) {}
async createUserWithProfile(data: CreateUserInput) {
// Use repository's transaction method
return this.repos.users.transaction(async (trx) => {
const user = await trx
.insertInto('users')
.values(data)
.returningAll()
.executeTakeFirstOrThrow()
await trx
.insertInto('profiles')
.values({ userId: user.id })
.execute()
return user
})
}
}
describe('UserService', () => {
it('should create user with profile', async () => {
await testInTransaction(db, async (trx) => {
const service = new UserService(createRepos(trx))
const user = await service.createUserWithProfile({
email: 'test@example.com',
name: 'Test'
})
expect(user.id).toBeDefined()
const profile = await trx
.selectFrom('profiles')
.where('user_id', '=', user.id)
.executeTakeFirst()
expect(profile).toBeDefined()
})
})
})
Testing Transactions
Verify transaction rollback behavior:
it('should rollback on error', async () => {
const initialCount = await countRows(db, 'users')
await expect(
db.transaction().execute(async (trx) => {
const repos = createRepos(trx)
await repos.users.create({ email: 'test@test.com', name: 'Test' })
throw new Error('Force rollback')
})
).rejects.toThrow('Force rollback')
// Verify rollback
const finalCount = await countRows(db, 'users')
expect(finalCount).toBe(initialCount)
})
Testing Plugins
Test plugin behavior with soft delete:
import { createORM, createRepositoryFactory, nativeAdapter } from '@kysera/repository'
import { softDeletePlugin } from '@kysera/soft-delete'
describe('Soft Delete Plugin', () => {
it('should soft delete user', async () => {
await testInTransaction(db, async (trx) => {
// Create executor with soft delete plugin using createORM
const orm = await createORM(trx, [softDeletePlugin()])
// Create repository using orm's createRepository
const userRepo = orm.createRepository((executor) => {
const factory = createRepositoryFactory(executor)
return factory.create({
tableName: 'users',
mapRow: (row) => row,
schemas: {
create: nativeAdapter(),
},
})
})
// Create and soft delete user
const user = await userRepo.create({
email: 'test@example.com',
name: 'Test User'
})
await userRepo.softDelete(user.id)
// Should not find with regular query
const found = await userRepo.findById(user.id)
expect(found).toBeNull()
// Should find with findWithDeleted
const foundDeleted = await userRepo.findWithDeleted(user.id)
expect(foundDeleted).toBeDefined()
expect(foundDeleted?.deleted_at).toBeDefined()
})
})
})
Database Cleanup Strategies
Transaction (Fastest)
await testInTransaction(db, async (trx) => {
// Test code - auto rollback
})
Delete (Preserves Sequences)
beforeEach(async () => {
await cleanDatabase(db, 'delete', ['users', 'posts'])
})
Truncate (Most Thorough)
afterAll(async () => {
await cleanDatabase(db, 'truncate')
})
Integration Testing
Test with real database:
import { seedDatabase, cleanDatabase } from '@kysera/testing'
describe('Integration', () => {
beforeAll(async () => {
// seedDatabase takes a function, not raw data
await seedDatabase(db, async (trx) => {
await trx
.insertInto('users')
.values([
{ email: 'alice@example.com', name: 'Alice', status: 'active' },
{ email: 'bob@example.com', name: 'Bob', status: 'active' },
])
.execute()
await trx
.insertInto('posts')
.values([
{ user_id: 1, title: 'Post 1' },
{ user_id: 2, title: 'Post 2' },
])
.execute()
})
})
afterAll(async () => {
await cleanDatabase(db, 'truncate')
})
it('should handle complex query', async () => {
const result = await db
.selectFrom('users')
.innerJoin('posts', 'posts.user_id', 'users.id')
.where('users.status', '=', 'active')
.select(['users.id', 'users.name', db.fn.count('posts.id').as('post_count')])
.groupBy(['users.id', 'users.name'])
.execute()
expect(result.length).toBeGreaterThan(0)
})
})
Testing with Vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
setupFiles: ['./tests/setup.ts'],
pool: 'forks', // Isolated processes for DB tests
}
})
// tests/setup.ts
import { db } from './db'
beforeAll(async () => {
// Run migrations
await runMigrations(db, migrations)
})
afterAll(async () => {
await db.destroy()
})
Best Practices
1. Use Transaction Isolation
// Each test is isolated
await testInTransaction(db, async (trx) => { ... })
2. Create Fresh Data Per Test
it('test 1', async () => {
await testInTransaction(db, async (trx) => {
const user = await createTestUser(trx) // Fresh data
// Test...
})
})
3. Test Edge Cases
it('should handle not found', async () => {
await testInTransaction(db, async (trx) => {
const repos = createRepos(trx)
const found = await repos.users.findById(999999)
expect(found).toBeNull()
})
})
it('should handle duplicate', async () => {
await testInTransaction(db, async (trx) => {
const repos = createRepos(trx)
await repos.users.create({ email: 'test@test.com', ... })
await expect(
repos.users.create({ email: 'test@test.com', ... })
).rejects.toThrow(UniqueConstraintError)
})
})
4. Test Validation
it('should validate input', async () => {
await testInTransaction(db, async (trx) => {
const repos = createRepos(trx)
await expect(
repos.users.create({ email: 'invalid', name: '' })
).rejects.toThrow()
})
})