Skip to content

Testing and Unit Testing - Oktuple

Overview

This document outlines Oktuple's comprehensive testing strategy, covering unit testing, integration testing, end-to-end testing, and testing best practices to ensure code quality and system reliability.

Testing Strategy

Testing Pyramid

Oktuple follows the testing pyramid approach to ensure comprehensive coverage:

Testing Principles

  1. Test-Driven Development (TDD): Write tests before implementation
  2. Comprehensive Coverage: Aim for >90% code coverage
  3. Fast Feedback: Unit tests should run in milliseconds
  4. Isolation: Tests should be independent and repeatable
  5. Realistic Scenarios: Test real-world usage patterns

Unit Testing

Framework Setup

Oktuple uses vitest as the primary testing framework for unit tests:

typescript
// Jest configuration
export default {
  preset: "ts-jest",
  testEnvironment: "node",
  roots: ["<rootDir>/src"],
  testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
  transform: {
    "^.+\\.ts$": "ts-jest",
  },
  collectCoverageFrom: [
    "src/**/*.ts",
    "!src/**/*.d.ts",
    "!src/**/__tests__/**",
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 90,
      statements: 90,
    },
  },
};

Unit Test Examples

Domain Model Testing

typescript
// __tests__/domain/Enode.test.ts
import { Enode } from "../src/domain/Enode";
import { Issue } from "../src/domain/Issue";
import { People } from "../src/domain/People";

describe("Enode", () => {
  let enode: Enode;

  beforeEach(() => {
    enode = new Enode({
      id: "enode-1",
      name: "Test Project",
      type: "project",
      status: "active",
      metadata: { description: "Test description" },
    });
  });

  describe("creation", () => {
    it("should create an enode with required properties", () => {
      expect(enode.id).toBe("enode-1");
      expect(enode.name).toBe("Test Project");
      expect(enode.type).toBe("project");
      expect(enode.status).toBe("active");
    });

    it("should generate creation timestamp", () => {
      expect(enode.createdAt).toBeInstanceOf(Date);
      expect(enode.updatedAt).toBeInstanceOf(Date);
    });
  });

  describe("status management", () => {
    it("should allow status transitions", () => {
      enode.updateStatus("completed");
      expect(enode.status).toBe("completed");
      expect(enode.updatedAt.getTime()).toBeGreaterThan(
        enode.createdAt.getTime()
      );
    });

    it("should validate status values", () => {
      expect(() => enode.updateStatus("invalid-status")).toThrow();
    });
  });

  describe("metadata management", () => {
    it("should update metadata", () => {
      enode.updateMetadata({ priority: "high", owner: "team-a" });
      expect(enode.metadata.priority).toBe("high");
      expect(enode.metadata.owner).toBe("team-a");
    });

    it("should preserve existing metadata", () => {
      enode.updateMetadata({ priority: "high" });
      expect(enode.metadata.description).toBe("Test description");
      expect(enode.metadata.priority).toBe("high");
    });
  });
});

Service Layer Testing

typescript
// __tests__/services/EnodeService.test.ts
import { EnodeService } from "../src/services/EnodeService";
import { EnodeRepository } from "../src/repositories/EnodeRepository";
import { EventBus } from "../src/events/EventBus";
import { Enode } from "../src/domain/Enode";

// Mock dependencies
jest.mock("../src/repositories/EnodeRepository");
jest.mock("../src/events/EventBus");

describe("EnodeService", () => {
  let enodeService: EnodeService;
  let mockRepository: jest.Mocked<EnodeRepository>;
  let mockEventBus: jest.Mocked<EventBus>;

  beforeEach(() => {
    mockRepository = new EnodeRepository() as jest.Mocked<EnodeRepository>;
    mockEventBus = new EventBus() as jest.Mocked<EventBus>;
    enodeService = new EnodeService(mockRepository, mockEventBus);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe("createEnode", () => {
    it("should create and persist enode", async () => {
      const enodeData = {
        name: "New Project",
        type: "project",
        status: "active",
      };

      const createdEnode = new Enode({
        id: "enode-2",
        ...enodeData,
      });

      mockRepository.create.mockResolvedValue(createdEnode);
      mockEventBus.emit.mockResolvedValue(undefined);

      const result = await enodeService.createEnode(enodeData);

      expect(mockRepository.create).toHaveBeenCalledWith(enodeData);
      expect(mockEventBus.emit).toHaveBeenCalledWith("enode.created", {
        enodeId: "enode-2",
        timestamp: expect.any(Date),
      });
      expect(result).toEqual(createdEnode);
    });

    it("should handle repository errors", async () => {
      const error = new Error("Database connection failed");
      mockRepository.create.mockRejectedValue(error);

      await expect(
        enodeService.createEnode({ name: "Test", type: "project" })
      ).rejects.toThrow("Database connection failed");
    });
  });

  describe("getEnode", () => {
    it("should retrieve enode by id", async () => {
      const enode = new Enode({
        id: "enode-1",
        name: "Test Project",
        type: "project",
      });

      mockRepository.findById.mockResolvedValue(enode);

      const result = await enodeService.getEnode("enode-1");

      expect(mockRepository.findById).toHaveBeenCalledWith("enode-1");
      expect(result).toEqual(enode);
    });

    it("should return null for non-existent enode", async () => {
      mockRepository.findById.mockResolvedValue(null);

      const result = await enodeService.getEnode("non-existent");

      expect(result).toBeNull();
    });
  });
});

Repository Testing

typescript
// __tests__/repositories/EnodeRepository.test.ts
import { EnodeRepository } from "../src/repositories/EnodeRepository";
import { PrismaClient } from "@prisma/client";
import { Enode } from "../src/domain/Enode";

jest.mock("@prisma/client");

describe("EnodeRepository", () => {
  let repository: EnodeRepository;
  let mockPrisma: jest.Mocked<PrismaClient>;

  beforeEach(() => {
    mockPrisma = new PrismaClient() as jest.Mocked<PrismaClient>;
    repository = new EnodeRepository(mockPrisma);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe("create", () => {
    it("should create enode in database", async () => {
      const enodeData = {
        name: "Test Project",
        type: "project",
        status: "active",
        metadata: { description: "Test" },
      };

      const dbEnode = {
        id: "enode-1",
        ...enodeData,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockPrisma.enode.create.mockResolvedValue(dbEnode);

      const result = await repository.create(enodeData);

      expect(mockPrisma.enode.create).toHaveBeenCalledWith({
        data: enodeData,
      });
      expect(result).toBeInstanceOf(Enode);
      expect(result.id).toBe("enode-1");
    });
  });

  describe("findById", () => {
    it("should find enode by id", async () => {
      const dbEnode = {
        id: "enode-1",
        name: "Test Project",
        type: "project",
        status: "active",
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockPrisma.enode.findUnique.mockResolvedValue(dbEnode);

      const result = await repository.findById("enode-1");

      expect(mockPrisma.enode.findUnique).toHaveBeenCalledWith({
        where: { id: "enode-1" },
      });
      expect(result).toBeInstanceOf(Enode);
    });

    it("should return null for non-existent enode", async () => {
      mockPrisma.enode.findUnique.mockResolvedValue(null);

      const result = await repository.findById("non-existent");

      expect(result).toBeNull();
    });
  });
});

Integration Testing

API Testing

typescript
// __tests__/integration/api/enode.test.ts
import request from "supertest";
import { app } from "../../../src/app";
import { PrismaClient } from "@prisma/client";
import { createTestUser, createTestEnode } from "../../helpers/testHelpers";

const prisma = new PrismaClient();

describe("Enode API Integration", () => {
  let authToken: string;
  let testUserId: string;

  beforeAll(async () => {
    const user = await createTestUser();
    testUserId = user.id;
    authToken = user.generateAuthToken();
  });

  afterAll(async () => {
    await prisma.user.delete({ where: { id: testUserId } });
    await prisma.$disconnect();
  });

  beforeEach(async () => {
    await prisma.enode.deleteMany();
  });

  describe("POST /api/enodes", () => {
    it("should create new enode", async () => {
      const enodeData = {
        name: "Integration Test Project",
        type: "project",
        status: "active",
      };

      const response = await request(app)
        .post("/api/enodes")
        .set("Authorization", `Bearer ${authToken}`)
        .send(enodeData)
        .expect(201);

      expect(response.body).toMatchObject({
        id: expect.any(String),
        name: enodeData.name,
        type: enodeData.type,
        status: enodeData.status,
        createdBy: testUserId,
      });

      // Verify database persistence
      const savedEnode = await prisma.enode.findUnique({
        where: { id: response.body.id },
      });
      expect(savedEnode).toBeTruthy();
    });

    it("should validate required fields", async () => {
      const response = await request(app)
        .post("/api/enodes")
        .set("Authorization", `Bearer ${authToken}`)
        .send({})
        .expect(400);

      expect(response.body.errors).toContainEqual({
        field: "name",
        message: "Name is required",
      });
    });
  });

  describe("GET /api/enodes/:id", () => {
    it("should retrieve enode by id", async () => {
      const enode = await createTestEnode(testUserId);

      const response = await request(app)
        .get(`/api/enodes/${enode.id}`)
        .set("Authorization", `Bearer ${authToken}`)
        .expect(200);

      expect(response.body).toMatchObject({
        id: enode.id,
        name: enode.name,
        type: enode.type,
      });
    });

    it("should return 404 for non-existent enode", async () => {
      await request(app)
        .get("/api/enodes/non-existent-id")
        .set("Authorization", `Bearer ${authToken}`)
        .expect(404);
    });
  });
});

Database Integration Testing

typescript
// __tests__/integration/database/enode.test.ts
import { PrismaClient } from "@prisma/client";
import { EnodeRepository } from "../../../src/repositories/EnodeRepository";
import {
  createTestDatabase,
  cleanupTestDatabase,
} from "../../helpers/dbHelpers";

describe("Enode Database Integration", () => {
  let prisma: PrismaClient;
  let repository: EnodeRepository;

  beforeAll(async () => {
    prisma = await createTestDatabase();
    repository = new EnodeRepository(prisma);
  });

  afterAll(async () => {
    await cleanupTestDatabase(prisma);
  });

  beforeEach(async () => {
    await prisma.enode.deleteMany();
  });

  describe("CRUD operations", () => {
    it("should perform full CRUD cycle", async () => {
      // Create
      const enodeData = {
        name: "CRUD Test Project",
        type: "project",
        status: "active",
      };

      const created = await repository.create(enodeData);
      expect(created).toBeInstanceOf(Enode);
      expect(created.name).toBe(enodeData.name);

      // Read
      const found = await repository.findById(created.id);
      expect(found).toBeTruthy();
      expect(found?.name).toBe(enodeData.name);

      // Update
      const updated = await repository.update(created.id, {
        status: "completed",
      });
      expect(updated?.status).toBe("completed");

      // Delete
      await repository.delete(created.id);
      const deleted = await repository.findById(created.id);
      expect(deleted).toBeNull();
    });
  });

  describe("Query operations", () => {
    beforeEach(async () => {
      await repository.create({
        name: "Project A",
        type: "project",
        status: "active",
      });
      await repository.create({
        name: "Project B",
        type: "project",
        status: "completed",
      });
      await repository.create({
        name: "Task A",
        type: "task",
        status: "active",
      });
    });

    it("should filter by type", async () => {
      const projects = await repository.findByType("project");
      expect(projects).toHaveLength(2);
      expect(projects.every((p) => p.type === "project")).toBe(true);
    });

    it("should filter by status", async () => {
      const activeItems = await repository.findByStatus("active");
      expect(activeItems).toHaveLength(2);
      expect(activeItems.every((p) => p.status === "active")).toBe(true);
    });
  });
});

End-to-End Testing

Playwright Setup

typescript
// playwright.config.ts
import { PlaywrightTestConfig } from "@playwright/test";

const config: PlaywrightTestConfig = {
  testDir: "./e2e",
  timeout: 30000,
  expect: {
    timeout: 5000,
  },
  use: {
    baseURL: process.env.TEST_BASE_URL || "http://localhost:3000",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
  },
  projects: [
    {
      name: "chromium",
      use: { browserName: "chromium" },
    },
    {
      name: "firefox",
      use: { browserName: "firefox" },
    },
    {
      name: "webkit",
      use: { browserName: "webkit" },
    },
  ],
};

export default config;

E2E Test Examples

typescript
// e2e/enode-management.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Enode Management", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/login");
    await page.fill('[data-testid="email"]', "test@example.com");
    await page.fill('[data-testid="password"]', "password123");
    await page.click('[data-testid="login-button"]');
    await page.waitForURL("/dashboard");
  });

  test("should create new enode", async ({ page }) => {
    await page.goto("/enodes");
    await page.click('[data-testid="create-enode-button"]');

    await page.fill('[data-testid="enode-name"]', "E2E Test Project");
    await page.selectOption('[data-testid="enode-type"]', "project");
    await page.click('[data-testid="save-enode-button"]');

    await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
    await expect(page.locator("text=E2E Test Project")).toBeVisible();
  });

  test("should edit existing enode", async ({ page }) => {
    await page.goto("/enodes");
    await page.click('[data-testid="enode-row"]:first-child');
    await page.click('[data-testid="edit-button"]');

    await page.fill('[data-testid="enode-name"]', "Updated Project Name");
    await page.click('[data-testid="save-button"]');

    await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
    await expect(page.locator("text=Updated Project Name")).toBeVisible();
  });

  test("should delete enode", async ({ page }) => {
    await page.goto("/enodes");
    const initialCount = await page
      .locator('[data-testid="enode-row"]')
      .count();

    await page.click('[data-testid="enode-row"]:first-child');
    await page.click('[data-testid="delete-button"]');
    await page.click('[data-testid="confirm-delete"]');

    await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
    const finalCount = await page.locator('[data-testid="enode-row"]').count();
    expect(finalCount).toBe(initialCount - 1);
  });
});

Testing Utilities and Helpers

Test Data Factories

typescript
// __tests__/helpers/factories.ts
import { Enode, Issue, People } from "../../src/domain";
import { faker } from "@faker-js/faker";

export class TestDataFactory {
  static createEnode(overrides: Partial<Enode> = {}): Enode {
    return new Enode({
      id: faker.string.uuid(),
      name: faker.company.name(),
      type: faker.helpers.arrayElement(["project", "task", "milestone"]),
      status: faker.helpers.arrayElement(["active", "completed", "paused"]),
      metadata: {},
      ...overrides,
    });
  }

  static createIssue(overrides: Partial<Issue> = {}): Issue {
    return new Issue({
      id: faker.string.uuid(),
      title: faker.lorem.sentence(),
      description: faker.lorem.paragraph(),
      priority: faker.helpers.arrayElement([
        "low",
        "medium",
        "high",
        "critical",
      ]),
      status: faker.helpers.arrayElement([
        "open",
        "in-progress",
        "resolved",
        "closed",
      ]),
      ...overrides,
    });
  }

  static createPeople(overrides: Partial<People> = {}): People {
    return new People({
      id: faker.string.uuid(),
      name: faker.person.fullName(),
      email: faker.internet.email(),
      role: faker.helpers.arrayElement(["developer", "manager", "stakeholder"]),
      ...overrides,
    });
  }
}

Mock Factories

typescript
// __tests__/helpers/mocks.ts
import { EventBus } from "../../src/events/EventBus";
import { EnodeRepository } from "../../src/repositories/EnodeRepository";

export const createMockEventBus = () =>
  ({
    emit: jest.fn().mockResolvedValue(undefined),
    on: jest.fn(),
    off: jest.fn(),
    subscribe: jest.fn(),
  } as jest.Mocked<EventBus>);

export const createMockEnodeRepository = () =>
  ({
    create: jest.fn(),
    findById: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
    findByType: jest.fn(),
    findByStatus: jest.fn(),
  } as jest.Mocked<EnodeRepository>);

Performance Testing

Load Testing with Artillery

yaml
# artillery.config.yml
config:
  target: "http://localhost:3000"
  phases:
    - duration: 60
      arrivalRate: 10
      name: "Warm up"
    - duration: 300
      arrivalRate: 50
      name: "Sustained load"
    - duration: 60
      arrivalRate: 100
      name: "Peak load"
  defaults:
    headers:
      Authorization: "Bearer {{ $randomString() }}"

scenarios:
  - name: "Enode CRUD operations"
    weight: 70
    flow:
      - get:
          url: "/api/enodes"
      - post:
          url: "/api/enodes"
          json:
            name: "Load Test Project"
            type: "project"
            status: "active"
      - think: 1
      - get:
          url: "/api/enodes/{{ enodeId }}"
      - put:
          url: "/api/enodes/{{ enodeId }}"
          json:
            status: "completed"

  - name: "Search operations"
    weight: 30
    flow:
      - get:
          url: "/api/enodes/search?q=test"
      - think: 2
      - get:
          url: "/api/enodes?type=project&status=active"

Testing Best Practices

Code Organization

  1. Test Structure: Mirror source code structure
  2. Naming Convention: *.test.ts or *.spec.ts
  3. Test Isolation: Each test should be independent
  4. Setup/Teardown: Use beforeEach and afterEach hooks

Test Data Management

  1. Factory Pattern: Use factories for test data creation
  2. Cleanup: Always clean up test data
  3. Randomization: Use faker for realistic test data
  4. Constants: Define test constants for consistency

Assertions and Expectations

  1. Specific Assertions: Test specific behavior, not implementation
  2. Error Testing: Test both success and failure scenarios
  3. Edge Cases: Test boundary conditions and error states
  4. Async Testing: Properly handle asynchronous operations

Mocking Strategy

  1. External Dependencies: Mock external services and APIs
  2. Database: Use test databases or in-memory alternatives
  3. Time: Mock time-dependent operations
  4. Randomness: Control random behavior in tests

Continuous Integration

GitHub Actions Workflow

yaml
# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:14
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run linting
        run: npm run lint

      - name: Run unit tests
        run: npm run test:unit

      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

Coverage Reporting

Coverage Configuration

typescript
// jest.config.ts coverage section
collectCoverageFrom: [
  'src/**/*.ts',
  '!src/**/*.d.ts',
  '!src/**/__tests__/**',
  '!src/**/index.ts',
  '!src/**/types.ts'
],
coverageReporters: [
  'text',
  'lcov',
  'html',
  'json'
],
coverageThreshold: {
  global: {
    branches: 80,
    functions: 80,
    lines: 90,
    statements: 90
  }
}

Coverage Badges

markdown
<!-- README.md -->

[![Tests](https://github.com/oktuple/oktuple/actions/workflows/test.yml/badge.svg)](https://github.com/oktuple/oktuple/actions/workflows/test.yml)
[![Coverage](https://codecov.io/gh/oktuple/oktuple/branch/main/graph/badge.svg)](https://codecov.io/gh/oktuple/oktuple)

Conclusion

Oktuple's testing strategy ensures code quality, system reliability, and maintainability through comprehensive testing at all levels. The combination of unit tests, integration tests, and end-to-end tests provides confidence in the system's behavior and enables safe refactoring and feature development.

Key benefits of this testing approach:

  • Early Bug Detection: Issues are caught during development
  • Refactoring Safety: Tests provide confidence when changing code
  • Documentation: Tests serve as living documentation
  • Quality Assurance: Consistent code quality across the codebase
  • Team Confidence: Developers can make changes with confidence

For more information on specific testing scenarios or to contribute to the testing suite, refer to the testing guidelines in the development documentation.