Testing Guide
Comprehensive guide to testing DBackup, including unit tests, integration tests, and manual testing procedures.
Test Infrastructure
Test Stack
# docker-compose.test.yml (simplified example)
services:
mysql-57:
image: mysql:5.7
ports:
- "33357:3306"
environment:
MYSQL_ROOT_PASSWORD: rootpassword
mysql-8:
image: mysql:8.0
ports:
- "33380:3306"
environment:
MYSQL_ROOT_PASSWORD: rootpassword
postgres-15:
image: postgres:15-alpine
ports:
- "54415:5432"
environment:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
mongodb:
image: mongo:6.0
ports:
- "27017:27017"Start Test Databases
docker-compose -f docker-compose.test.yml up -dTest Credentials
| Database | Host | Port | User | Password |
|---|---|---|---|---|
| MySQL 5.7 | localhost | 33357 | root | rootpassword |
| MySQL 8.0 | localhost | 33380 | root | rootpassword |
| MySQL 9.1 | localhost | 33390 | root | rootpassword |
| MariaDB 10 | localhost | 33310 | root | rootpassword |
| MariaDB 11 | localhost | 33311 | root | rootpassword |
| PostgreSQL 12-17 | localhost | 54412-54417 | testuser | testpassword |
| MongoDB | localhost | 27017 | - | - |
Test Categories
Unit Tests
Test pure functions and isolated logic.
Location: tests/unit/ or co-located with source files
Run:
pnpm testExample (Retention):
// tests/unit/retention-service.test.ts
import { describe, it, expect } from "vitest";
import { RetentionService } from "@/services/retention-service";
describe("RetentionService", () => {
describe("calculateRetention", () => {
it("keeps correct number of daily backups", () => {
const files = generateTestFiles(30);
const config = {
mode: "SMART",
smart: { daily: 7, weekly: 0, monthly: 0, yearly: 0 },
};
const result = RetentionService.calculateRetention(files, config);
expect(result.keep.length).toBe(7);
expect(result.delete.length).toBe(23);
});
it("never deletes locked files", () => {
const files = [
{ name: "backup-1", locked: true, modifiedAt: new Date() },
{ name: "backup-2", locked: false, modifiedAt: new Date() },
];
const config = { mode: "SIMPLE", simple: { keepCount: 1 } };
const result = RetentionService.calculateRetention(files, config);
expect(result.keep.map(f => f.name)).toContain("backup-1");
});
});
});Example (Checksum):
// tests/unit/lib/checksum.test.ts
import { describe, it, expect } from "vitest";
import { calculateChecksum, calculateFileChecksum, verifyFileChecksum } from "@/lib/checksum";
import fs from "fs/promises";
describe("calculateChecksum", () => {
it("returns consistent SHA-256 hash", () => {
const hash1 = calculateChecksum("hello world");
const hash2 = calculateChecksum("hello world");
expect(hash1).toBe(hash2);
expect(hash1).toHaveLength(64); // SHA-256 = 64 hex chars
});
it("produces different hashes for different inputs", () => {
expect(calculateChecksum("hello")).not.toBe(calculateChecksum("world"));
});
});
describe("verifyFileChecksum", () => {
it("detects file modification", async () => {
const tmpPath = "/tmp/test-checksum-verify.txt";
await fs.writeFile(tmpPath, "original content");
const hash = await calculateFileChecksum(tmpPath);
// Modify file
await fs.writeFile(tmpPath, "modified content");
const result = await verifyFileChecksum(tmpPath, hash);
expect(result.valid).toBe(false);
await fs.unlink(tmpPath);
});
});Test adapters against real database instances.
Location: tests/integration/
Prerequisites:
- Docker containers running
- CLI tools installed (
mysqldump,pg_dump, etc.)
Run:
pnpm test:integrationExample:
// tests/integration/adapters/mysql.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { MySQLAdapter } from "@/lib/adapters/database/mysql";
import { exec } from "child_process";
import { promisify } from "util";
import fs from "fs/promises";
const execAsync = promisify(exec);
describe("MySQLAdapter Integration", () => {
const config = {
host: "localhost",
port: 3306,
username: "root",
password: "rootpassword",
database: "test_db",
};
beforeAll(async () => {
// Create test database
await execAsync(
`mysql -h${config.host} -P${config.port} ` +
`-u${config.username} -p${config.password} ` +
`-e "CREATE DATABASE IF NOT EXISTS test_db"`
);
// Insert test data
await execAsync(
`mysql -h${config.host} -P${config.port} ` +
`-u${config.username} -p${config.password} test_db ` +
`-e "CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(100))"`
);
});
afterAll(async () => {
// Cleanup
await execAsync(
`mysql -h${config.host} -P${config.port} ` +
`-u${config.username} -p${config.password} ` +
`-e "DROP DATABASE IF EXISTS test_db"`
);
});
it("should test connection", async () => {
const result = await MySQLAdapter.test(config);
expect(result.success).toBe(true);
});
it("should dump database", async () => {
const backupPath = "/tmp/test-mysql-backup.sql";
const result = await MySQLAdapter.dump(config, backupPath);
expect(result.success).toBe(true);
expect(result.size).toBeGreaterThan(0);
const content = await fs.readFile(backupPath, "utf-8");
expect(content).toContain("CREATE TABLE");
await fs.unlink(backupPath);
});
it("should list databases", async () => {
const databases = await MySQLAdapter.getDatabases(config);
expect(databases).toContain("test_db");
expect(databases).not.toContain("information_schema");
});
});E2E Tests
End-to-end tests through the UI.
Manual Process:
- Start dev server:
pnpm dev - Create test source/destination
- Create and run test job
- Verify backup in Storage Explorer
- Test restore functionality
Automated (future):
pnpm test:e2e # Playwright testsTest Utilities
Generate Test Data
# Seed test databases with data
pnpm test:uiThis script:
- Creates test databases in MySQL/PostgreSQL/MongoDB
- Inserts sample data
- Seeds the local DBackup database with pre-configured sources
Stress Testing
Generate large datasets:
# Generate 1GB of test data
./scripts/generate-stress-data.sh mysql 1000000
# Test backup performance
time pnpm test:integration -- --grep "large database"Testing Adapters
Database Adapter Tests
// Template for database adapter tests
describe("DatabaseAdapter", () => {
const testCases = [
{ name: "MySQL 8.0", config: mysqlConfig },
{ name: "PostgreSQL 15", config: postgresConfig },
{ name: "MongoDB 6.0", config: mongoConfig },
];
testCases.forEach(({ name, config }) => {
describe(name, () => {
it("tests connection", async () => {
const result = await adapter.test(config);
expect(result.success).toBe(true);
});
it("dumps database", async () => {
const result = await adapter.dump(config, "/tmp/backup");
expect(result.success).toBe(true);
});
it("restores database", async () => {
// Dump first
await adapter.dump(config, "/tmp/backup");
// Restore to different database
const result = await adapter.restore(
{ ...config, database: "test_restore" },
"/tmp/backup"
);
expect(result.success).toBe(true);
});
});
});
});Storage Adapter Tests
describe("StorageAdapter", () => {
const testFile = "/tmp/test-file.txt";
const remotePath = "test/file.txt";
beforeAll(async () => {
await fs.writeFile(testFile, "test content");
});
it("uploads file", async () => {
await adapter.upload(config, testFile, remotePath);
const files = await adapter.list(config, "test");
expect(files.map(f => f.name)).toContain("file.txt");
});
it("downloads file", async () => {
const downloadPath = "/tmp/downloaded.txt";
await adapter.download(config, remotePath, downloadPath);
const content = await fs.readFile(downloadPath, "utf-8");
expect(content).toBe("test content");
});
it("deletes file", async () => {
await adapter.delete(config, remotePath);
const files = await adapter.list(config, "test");
expect(files.map(f => f.name)).not.toContain("file.txt");
});
});Debugging Tests
Vitest UI
pnpm test --uiOpens interactive test runner at http://localhost:51204.
Debug Mode
# Run specific test with debugging
DEBUG=* pnpm test -- --grep "MySQL"Prisma Studio
Inspect database during tests:
npx prisma studioRunner Debugging
- Set breakpoints in VS Code
- Create manual trigger job in UI
- Trigger job execution
- Step through runner code
Tip: Temp files are in /tmp. Check there if backup fails before cleanup.
Common Issues
CLI Tools Not Found
Error: Command not found: mysqldumpFix (macOS):
brew install mysql-client
export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"Fix (Ubuntu):
sudo apt install mysql-clientConnection Refused
Error: connect ECONNREFUSED 127.0.0.1:3306Fix: Ensure Docker containers are running:
docker ps
docker-compose -f docker-compose.test.yml up -dPermission Denied
Error: Access denied for user 'root'@'172.17.0.1'Fix: Check container logs and credentials:
docker logs dbackup-mysql-testCI/CD Integration
GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: rootpassword
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
postgres:
image: postgres:15
env:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Install CLI tools
run: |
sudo apt-get update
sudo apt-get install -y mysql-client postgresql-client
- name: Run unit tests
run: pnpm test
- name: Run integration tests
run: pnpm test:integrationTest Coverage
Generate coverage report:
pnpm test -- --coverageCoverage thresholds in vitest.config.ts:
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "html"],
thresholds: {
lines: 70,
functions: 70,
branches: 60,
},
},
},
});