Zequel uses a comprehensive testing strategy with three test types:
Unit tests - Fast, isolated tests with mocked dependencies
Integration tests - Tests against real databases via Docker
E2E tests - Full application tests with Playwright
Test Stack
Vitest Unit and integration test runner with Vite integration
Playwright End-to-end testing framework for Electron
Docker Compose Test database containers with seed data
Running Tests
Quick Commands
All Tests (Watch)
Unit Tests Only
Integration Tests
E2E Tests
Coverage Report
Interactive UI
npm run test
# Runs unit + integration tests in watch mode
# Re-runs on file changes
Pre-commit Hook
Husky runs these commands before each commit:
npm run typecheck # Type-check all code
npm run test:unit # Run unit tests
Commits are blocked if either fails.
Unit Tests
Overview
Location : src/tests/unit/
Characteristics :
Fast (no I/O, no database connections)
Isolated (all dependencies mocked)
Run on every commit (pre-commit hook)
Mirror source structure:
unit/main/ - Main process tests
unit/renderer/ - Renderer process tests
Configuration
File : vitest.config.ts
export default defineConfig ({
test: {
globals: true , // No need to import describe, it, expect
environment: 'node' , // Node.js environment
include: [ 'src/tests/**/*.{test,spec}.{ts,mts,js,mjs}' ],
exclude: [ 'node_modules' , 'src/tests/e2e/**' ],
coverage: {
provider: 'v8' ,
reporter: [ 'text' , 'html' ],
exclude: [ 'node_modules/' , 'src/tests/' ]
},
alias: {
'@' : resolve ( __dirname , './src/renderer' ),
'@main' : resolve ( __dirname , './src/main' )
}
}
})
Writing Unit Tests
Example : Testing a service (main process)
// src/tests/unit/main/connections.test.ts
import { describe , it , expect , vi , beforeEach } from 'vitest' ;
import { DatabaseType } from '@main/types' ;
import type { ConnectionConfig } from '@main/types' ;
// Mock dependencies
vi . mock ( '@main/utils/logger' , () => ({
logger: {
debug: vi . fn (),
info: vi . fn (),
error: vi . fn (),
},
}));
// Mock database
const mockPrepare = vi . fn (() => ({
run: vi . fn (() => ({ changes: 1 })),
get: vi . fn (),
all: vi . fn (() => []),
}));
vi . mock ( '@main/services/database' , () => ({
appDatabase: {
getDatabase: vi . fn (() => ({
prepare: mockPrepare ,
})),
},
}));
// Import after mocks
import { ConnectionsService } from '@main/services/connections' ;
describe ( 'ConnectionsService' , () => {
let service : ConnectionsService ;
beforeEach (() => {
vi . clearAllMocks ();
service = new ConnectionsService ();
});
it ( 'should save a connection' , async () => {
const config : ConnectionConfig = {
id: 'test-1' ,
name: 'Test DB' ,
type: DatabaseType . PostgreSQL ,
host: 'localhost' ,
port: 5432 ,
database: 'testdb' ,
username: 'user' ,
};
await service . save ( config );
expect ( mockPrepare ). toHaveBeenCalledWith ( expect . stringContaining ( 'INSERT' ));
});
it ( 'should list connections' , async () => {
const mockConnections = [
{ id: '1' , name: 'DB1' , type: 'postgresql' },
{ id: '2' , name: 'DB2' , type: 'mysql' },
];
mockPrepare . mockReturnValueOnce ({
all: vi . fn (() => mockConnections ),
});
const result = await service . list ();
expect ( result ). toHaveLength ( 2 );
expect ( result [ 0 ]. name ). toBe ( 'DB1' );
});
});
Example : Testing a composable (renderer)
// src/tests/unit/renderer/formatters.test.ts
import { describe , it , expect } from 'vitest' ;
import { formatBytes , formatDuration } from '@/lib/formatters' ;
describe ( 'formatBytes' , () => {
it ( 'should format bytes correctly' , () => {
expect ( formatBytes ( 0 )). toBe ( '0 B' );
expect ( formatBytes ( 1024 )). toBe ( '1 KB' );
expect ( formatBytes ( 1024 * 1024 )). toBe ( '1 MB' );
expect ( formatBytes ( 1024 * 1024 * 1024 )). toBe ( '1 GB' );
});
});
describe ( 'formatDuration' , () => {
it ( 'should format milliseconds' , () => {
expect ( formatDuration ( 500 )). toBe ( '500ms' );
expect ( formatDuration ( 1500 )). toBe ( '1.50s' );
expect ( formatDuration ( 65000 )). toBe ( '1m 5s' );
});
});
Mocking Guidelines
Always mock external dependencies
Mock file system, databases, network calls, and third-party libraries: vi . mock ( '@main/utils/logger' );
vi . mock ( '@main/services/database' );
vi . mock ( 'fs/promises' );
Mocks must be declared before importing the module under test: // Good
vi . mock ( '@main/utils/logger' );
import { service } from '@main/services/myService' ;
// Bad
import { service } from '@main/services/myService' ;
vi . mock ( '@main/utils/logger' ); // Too late!
Clear mocks between tests
Use beforeEach(() => vi.clearAllMocks()) to reset mock state: beforeEach (() => {
vi . clearAllMocks ();
});
Integration Tests
Overview
Location : src/tests/integration/main/
Characteristics :
Connect to real databases via Docker
Verify seed data integrity
Test database-specific features
Gracefully skip when containers unavailable
Setup
Start Docker Containers
Starts all database containers with seed data.
Writing Integration Tests
Example : Testing PostgreSQL seed data
// src/tests/integration/main/postgres-seed.test.ts
import { describe , it , expect , beforeAll , afterAll } from 'vitest' ;
import { PostgresDriver } from '@main/db/postgres' ;
import { DatabaseType } from '@main/types' ;
const config = {
id: 'test' ,
name: 'Test' ,
type: DatabaseType . PostgreSQL ,
host: 'localhost' ,
port: 54320 ,
database: 'zequel' ,
username: 'zequel' ,
password: 'zequel' ,
};
describe ( 'PostgreSQL seed data' , () => {
let driver : PostgresDriver ;
beforeAll ( async () => {
driver = new PostgresDriver ();
try {
await driver . connect ( config );
} catch ( err ) {
console . warn ( 'PostgreSQL container not available, skipping tests' );
return ;
}
});
afterAll ( async () => {
if ( driver ?. isConnected ) {
await driver . disconnect ();
}
});
it ( 'should have seed tables' , async () => {
if ( ! driver ?. isConnected ) return ;
const tables = await driver . getTables ( 'zequel' );
expect ( tables . find ( t => t . name === 'users' )). toBeDefined ();
expect ( tables . find ( t => t . name === 'products' )). toBeDefined ();
});
it ( 'should have seed data in users table' , async () => {
if ( ! driver ?. isConnected ) return ;
const result = await driver . execute ( 'SELECT COUNT(*) as count FROM users' );
expect ( Number ( result . rows [ 0 ]. count )). toBeGreaterThan ( 0 );
});
it ( 'should have sequences' , async () => {
if ( ! driver ?. isConnected ) return ;
const sequences = await driver . getSequences ();
expect ( sequences . length ). toBeGreaterThan ( 0 );
});
});
Database Connection Details
See Development Setup - Database Ports for all connection details.
Skip Pattern
Integration tests gracefully skip when Docker containers aren’t running:
beforeAll ( async () => {
try {
await driver . connect ( config );
} catch ( err ) {
console . warn ( 'Database not available, skipping tests' );
return ;
}
});
it ( 'should test feature' , async () => {
if ( ! driver ?. isConnected ) return ; // Skip if not connected
// Test logic...
});
End-to-End Tests
Overview
Location : src/tests/e2e/
Characteristics :
Test the full Electron app (main + renderer)
Playwright controls the application
Tests user workflows (connect, query, export, etc.)
Requires production build
Configuration
File : playwright.config.ts
export default defineConfig ({
tsconfig: './tsconfig.e2e.json' ,
testDir: './src/tests/e2e/tests' ,
timeout: 60_000 ,
expect: { timeout: 30_000 } ,
fullyParallel: false ,
workers: 1 ,
retries: 1 ,
reporter: 'list' ,
use: {
actionTimeout: 10_000 ,
trace: 'on-first-retry' ,
screenshot: 'only-on-failure' ,
} ,
})
Running E2E Tests
Build the App
E2E tests require a production build:
Run Tests
npm run test:e2e
# Or run a specific test:
npx playwright test src/tests/e2e/tests/connection.test.ts
Writing E2E Tests
Example : Testing connection workflow
// src/tests/e2e/tests/connection.test.ts
import { test , expect } from '@playwright/test' ;
import { launchApp } from '@e2e/utils/electron' ;
test . describe ( 'Connection Management' , () => {
test ( 'should create and connect to SQLite database' , async () => {
const { app , page } = await launchApp ();
try {
// Click "New Connection" button
await page . click ( 'button:has-text("New Connection")' );
// Fill connection form
await page . fill ( 'input[name="name"]' , 'Test SQLite' );
await page . selectOption ( 'select[name="type"]' , 'sqlite' );
await page . fill ( 'input[name="filepath"]' , '/tmp/test.db' );
// Save connection
await page . click ( 'button:has-text("Save")' );
// Verify connection appears in list
await expect ( page . locator ( 'text=Test SQLite' )). toBeVisible ();
// Connect
await page . click ( 'button:has-text("Connect")' );
// Verify connected state
await expect ( page . locator ( '.connection-status:has-text("Connected")' )). toBeVisible ();
} finally {
await app . close ();
}
});
});
Helpers
Electron launcher (src/tests/e2e/utils/electron.ts):
import { _electron as electron } from '@playwright/test' ;
export const launchApp = async () => {
const app = await electron . launch ({
args: [ '.' ],
env: {
... process . env ,
E2E: '1' , // Prevent auto-showing windows
},
});
const page = await app . firstWindow ();
return { app , page };
};
Best Practices
Test Behavior, Not Implementation Test what users see and do, not internal implementation details.
Use Descriptive Test Names it('should display error when connection fails') is better than it('works').
Keep Tests Independent Each test should run in isolation. Use beforeEach for setup.
Mock External Services Don’t depend on external APIs or services in unit tests.
Test Edge Cases Test error conditions, empty states, and boundary values.
Use AAA Pattern Arrange (setup), Act (execute), Assert (verify).
Coverage
Generate coverage reports:
Output : coverage/index.html
Open in browser to see line-by-line coverage:
open coverage/index.html # macOS
xdg-open coverage/index.html # Linux
start coverage/index.html # Windows
Coverage excludes src/tests/ and node_modules/ by default.
Debugging Tests
Vitest UI
Opens interactive UI at http://localhost:51204 with:
Test file tree
Console output
Coverage visualization
Re-run on file save
Playwright Inspector
PWDEBUG = 1 npx playwright test src/tests/e2e/tests/connection.test.ts
Opens Playwright Inspector for step-by-step debugging.
VS Code Debugging
Add to .vscode/launch.json:
{
"version" : "0.2.0" ,
"configurations" : [
{
"type" : "node" ,
"request" : "launch" ,
"name" : "Debug Vitest Tests" ,
"runtimeExecutable" : "npm" ,
"runtimeArgs" : [ "run" , "test:unit" ],
"console" : "integratedTerminal"
}
]
}
Set breakpoints and press F5 to debug.
Continuous Integration
GitHub Actions runs tests on every PR:
- name : Run type checking
run : npm run typecheck
- name : Run unit tests
run : npm run test:unit
- name : Run integration tests
run : |
docker-compose up -d
npm run test:integration
- name : Build app
run : npm run build
- name : Run E2E tests
run : npm run test:e2e
Next Steps
Database Adapters Learn how to add support for new databases
Project Structure Navigate the codebase