Appearance
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
- Test-Driven Development (TDD): Write tests before implementation
- Comprehensive Coverage: Aim for >90% code coverage
- Fast Feedback: Unit tests should run in milliseconds
- Isolation: Tests should be independent and repeatable
- 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
- Test Structure: Mirror source code structure
- Naming Convention:
*.test.tsor*.spec.ts - Test Isolation: Each test should be independent
- Setup/Teardown: Use
beforeEachandafterEachhooks
Test Data Management
- Factory Pattern: Use factories for test data creation
- Cleanup: Always clean up test data
- Randomization: Use faker for realistic test data
- Constants: Define test constants for consistency
Assertions and Expectations
- Specific Assertions: Test specific behavior, not implementation
- Error Testing: Test both success and failure scenarios
- Edge Cases: Test boundary conditions and error states
- Async Testing: Properly handle asynchronous operations
Mocking Strategy
- External Dependencies: Mock external services and APIs
- Database: Use test databases or in-memory alternatives
- Time: Mock time-dependent operations
- 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.infoCoverage 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 -->
[](https://github.com/oktuple/oktuple/actions/workflows/test.yml)
[](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.