Skip to content

Testing Guide

Comprehensive guide to testing DBackup, including unit tests, integration tests, and manual testing procedures.

Test Infrastructure

Test Stack

yaml
# 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

bash
docker-compose -f docker-compose.test.yml up -d

Test Credentials

DatabaseHostPortUserPassword
MySQL 5.7localhost33357rootrootpassword
MySQL 8.0localhost33380rootrootpassword
MySQL 9.1localhost33390rootrootpassword
MariaDB 10localhost33310rootrootpassword
MariaDB 11localhost33311rootrootpassword
PostgreSQL 12-17localhost54412-54417testusertestpassword
MongoDBlocalhost27017--

Test Categories

Unit Tests

Test pure functions and isolated logic.

Location: tests/unit/ or co-located with source files

Run:

bash
pnpm test

Example (Retention):

typescript
// 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):

typescript
// 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:

  1. Docker containers running
  2. CLI tools installed (mysqldump, pg_dump, etc.)

Run:

bash
pnpm test:integration

Example:

typescript
// 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:

  1. Start dev server: pnpm dev
  2. Create test source/destination
  3. Create and run test job
  4. Verify backup in Storage Explorer
  5. Test restore functionality

Automated (future):

bash
pnpm test:e2e  # Playwright tests

Test Utilities

Generate Test Data

bash
# Seed test databases with data
pnpm test:ui

This script:

  1. Creates test databases in MySQL/PostgreSQL/MongoDB
  2. Inserts sample data
  3. Seeds the local DBackup database with pre-configured sources

Stress Testing

Generate large datasets:

bash
# 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

typescript
// 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

typescript
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

bash
pnpm test --ui

Opens interactive test runner at http://localhost:51204.

Debug Mode

bash
# Run specific test with debugging
DEBUG=* pnpm test -- --grep "MySQL"

Prisma Studio

Inspect database during tests:

bash
npx prisma studio

Runner Debugging

  1. Set breakpoints in VS Code
  2. Create manual trigger job in UI
  3. Trigger job execution
  4. 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: mysqldump

Fix (macOS):

bash
brew install mysql-client
export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"

Fix (Ubuntu):

bash
sudo apt install mysql-client

Connection Refused

Error: connect ECONNREFUSED 127.0.0.1:3306

Fix: Ensure Docker containers are running:

bash
docker ps
docker-compose -f docker-compose.test.yml up -d

Permission Denied

Error: Access denied for user 'root'@'172.17.0.1'

Fix: Check container logs and credentials:

bash
docker logs dbackup-mysql-test

CI/CD Integration

GitHub Actions

yaml
# .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:integration

Test Coverage

Generate coverage report:

bash
pnpm test -- --coverage

Coverage thresholds in vitest.config.ts:

typescript
export default defineConfig({
  test: {
    coverage: {
      provider: "v8",
      reporter: ["text", "html"],
      thresholds: {
        lines: 70,
        functions: 70,
        branches: 60,
      },
    },
  },
});

Released under the GNU General Public License. | Privacy · Legal Notice