Testing Best Practices for Lockness
Overview
Lockness uses deterministic time control and in-memory mocks to keep tests fast, reliable, and hermetic. Follow these guidelines when writing tests for the framework.
Time Control with FakeTime
The Problem
Traditional time-based tests use real delays with setTimeout:
// ❌ Slow and non-deterministic
Deno.test('session expiration', async () => {
await driver.write('session', { id: 1 }, 1) // 1 second TTL
await new Promise((resolve) => setTimeout(resolve, 1100)) // Wait 1.1 seconds
assertEquals(await driver.read('session'), null)
})
This test takes over 1 second to run and can be flaky due to timing issues.
The Solution
Use FakeTime from @std/testing/time to replace real time delays with instant
time manipulation:
// ✅ Fast and deterministic
import { FakeTime } from '@std/testing/time'
Deno.test('session expiration', async () => {
using time = new FakeTime()
await driver.write('session', { id: 1 }, 1) // 1 second TTL
time.tick(1100) // Advance 1.1 seconds instantly
assertEquals(await driver.read('session'), null)
})
This test completes in milliseconds with no race conditions.
When to Use FakeTime
- Testing TTL/expiration logic (sessions, cache)
- Testing time-based delays or intervals
- Any test with
setTimeout,setInterval, orDate.now() - Testing scheduled jobs or queue delays
Benefits
- Tests run in milliseconds instead of seconds
- No race conditions from real timers
- Deterministic test execution
- Parallel-safe (no timer conflicts)
In-Memory Storage Mocks
The Problem
Traditional storage tests write to the filesystem:
// ❌ Slow and creates side effects
Deno.test('storage operations', async () => {
const driver = new LocalStorageDriver({
driver: 'local',
root: './tmp/test-storage',
})
await driver.put('file.txt', 'content')
assertEquals(await driver.get('file.txt'), 'content')
// Cleanup required
await driver.delete('file.txt')
})
This test:
- Writes to disk (slow)
- Creates
tmp/directories (filesystem pollution) - Requires cleanup
- Can't run in parallel safely
The Solution
Use in-memory mock drivers that implement the same interface:
// ✅ Fast and hermetic
import { createMockStorage } from '@lockness/storage/tests/support/mock_driver.ts'
Deno.test('storage operations', async () => {
const driver = createMockStorage()
await driver.put('file.txt', 'content')
assertEquals(await driver.get('file.txt'), 'content')
// No cleanup needed - all in memory
})
When to Use Memory Mocks
- Testing storage drivers (local, S3, R2)
- Testing file operations (put, get, delete, copy, move)
- Testing storage-dependent services
- Any test that writes to disk
Benefits
- Tests run 10-100x faster (no disk I/O)
- No filesystem pollution (no
tmp/directories) - Parallel-safe (no file conflicts)
- Hermetic (no side effects)
- No cleanup required
Performance Guidelines
Avoid Real Delays
❌ Never use setTimeout with actual time in tests:
// Bad
await new Promise((resolve) => setTimeout(resolve, 1000))
✅ Use FakeTime instead:
// Good
using time = new FakeTime()
time.tick(1000)
Minimize Micro-delays
If you must use real delays (e.g., for event loop processing), keep them minimal:
// Before
await new Promise((resolve) => setTimeout(resolve, 10))
// After
await new Promise((resolve) => setTimeout(resolve, 1))
Use Memory Drivers
❌ Avoid filesystem I/O in unit tests:
// Bad
const driver = new LocalStorageDriver({ driver: 'local', root: './tmp' })
✅ Use in-memory mocks:
// Good
const driver = createMockStorage()
Keep Tests Hermetic
Tests should not create side effects:
- No files written to disk
- No network calls to external services
- No shared state between tests
- No environment variable modifications
Test Suite Performance Targets
Target metrics for the full test suite:
| Package | Before | Target | Improvement |
|---|---|---|---|
| Session | 3s | < 1s | 3x faster |
| Cache | 2s | < 1s | 2x faster |
| Storage | 5s | < 2s | 2.5x faster |
| Events | 1s | < 0.5s | 2x faster |
| Total | 87s | < 30s | 3x faster |
Examples
Session Expiration Test
import { FakeTime } from '@std/testing/time'
import { MemorySessionDriver } from '@lockness/session'
Deno.test('MemorySessionDriver - session expiration', async () => {
using time = new FakeTime()
const driver = new MemorySessionDriver()
await driver.write('expire-session', { userId: 789 }, 1) // 1 second
time.tick(1100) // Advance 1.1 seconds
const retrieved = await driver.read('expire-session')
assertEquals(retrieved, null)
})
Cache TTL Test
import { FakeTime } from '@std/testing/time'
import { get, set } from '@lockness/cache'
Deno.test('cache TTL causes expiration', async () => {
using time = new FakeTime()
await set('expiring', 'value', 0.1) // 100ms TTL
assertEquals(await get('expiring'), 'value')
time.tick(150) // Advance 150ms
assertEquals(await get('expiring'), null)
})
Storage Mock Test
import { createMockStorage } from '@lockness/storage/tests/support/mock_driver.ts'
Deno.test('storage copy operation', async () => {
const driver = createMockStorage()
await driver.put('source.txt', 'Copy me')
await driver.copy('source.txt', 'destination.txt')
const source = await driver.get('source.txt')
const dest = await driver.get('destination.txt')
assertEquals(source, 'Copy me')
assertEquals(dest, 'Copy me')
})
Integration vs Unit Tests
Unit Tests (Use Mocks)
Unit tests should be fast and hermetic:
- Use
FakeTimefor time control - Use in-memory mocks for storage
- Mock external dependencies
- Run in < 100ms each
Integration Tests (Use Real Drivers)
Integration tests validate actual behavior:
- Use real database connections
- Use real storage drivers (S3, R2, Local)
- Test with real external services
- Slower but validate end-to-end behavior
Contributing
When adding new tests to the Lockness framework:
- ✅ Use
FakeTimefor time-based tests - ✅ Use in-memory mocks for storage tests
- ✅ Keep tests hermetic (no side effects)
- ✅ Target < 100ms per test
- ✅ Add tests to
tests/directory with*.test.tsnaming - ✅ Run
deno task testbefore committing
References
- @std/testing/time - FakeTime API
- GEMINI.md - Full testing guidelines
- packages/storage/tests/support/mock_driver.ts - Storage mock implementation