Savvi Studio

Integration vs Unit Test Mocks - Separation Guide

Date: November 7, 2025

Summary

The codebase has TWO separate mock/fixture systems that serve different purposes and should remain separate:

  1. Integration Test Infrastructure (tests/integration/setup/)
  2. Unit Test Mocks (tests/mocks/)

Integration Test Infrastructure

Location: tests/integration/setup/

Purpose: Support end-to-end integration tests that verify complete workflows with real database interactions

Files:

tests/integration/setup/
├── factories/
│   ├── database-fixtures.ts     # Create REAL database records
│   └── workos-events.ts          # WorkOS event data for integration tests
├── mocks.ts                      # Mock external APIs (Next.js, WorkOS)
├── fixtures.ts                   # Test data helpers
└── utilities/                    # DB helpers, cleanup utilities

What They Do:

  • database-fixtures.ts: Factory functions that create ACTUAL database records for testing

    • ensureNodeType() - Insert real node types
    • createResourceWithPermission() - Create real graph nodes and edges
    • setupTestWorkspace() - Create complete test hierarchies in database
  • mocks.ts: Mock EXTERNAL dependencies to prevent real API calls

    • Mock Next.js cookies() to avoid request scope errors
    • Mock WorkOS client to prevent real API calls
    • Mock session context for authenticated test scenarios
  • workos-events.ts: Factory functions for WorkOS webhook event payloads

Testing Approach:

// Integration test example
import { setupTestWorkspace } from '../setup/factories/database-fixtures';
import '../setup/mocks'; // Mock external APIs

describe('Full Workflow', () => {
  it('should handle complete user journey', async () => {
    // Create REAL database records
    const workspace = await setupTestWorkspace(client, {
      resourceCount: 3
    });
    
    // Test actual API endpoints and workflows
    const result = await someRealOperation(workspace.user.id);
    expect(result).toBeDefined();
  });
});

Unit Test Mocks

Location: tests/mocks/

Purpose: Provide mock objects for isolated unit testing without database or external dependencies

Files:

tests/mocks/
├── db/
│   ├── pool-client.mock.ts      # Mock PoolClient objects
│   ├── query-result.fixtures.ts # Mock query results
│   └── presets.ts                # Pre-configured mock scenarios
├── auth/                         # Auth mocks (to be implemented)
├── services/                     # Service mocks (to be implemented)
└── models/                       # Model fixtures (to be implemented)

What They Do:

  • Provide MOCK objects that simulate behavior without real implementation
  • Allow testing in complete isolation
  • No database connection required
  • Fast execution

Testing Approach:

// Unit test example
import { createPoolClientMock } from '@tests/mocks';

describe('Isolated Function', () => {
  it('should process data correctly', () => {
    // Use MOCK client (no real database)
    const client = createPoolClientMock({
      query: vi.fn().mockResolvedValue({ rows: [{ id: 1 }] })
    });
    
    // Test function logic in isolation
    const result = await someFunction(client);
    expect(result).toBe(expected);
  });
});

Key Differences

Aspect Integration Tests Unit Tests
Database Real PostgreSQL No database (mocked)
Speed Slower (DB I/O) Fast (no I/O)
Scope End-to-end workflows Single function/class
Setup Create real data Mock objects only
Cleanup Must clean up DB No cleanup needed
External APIs Mocked (WorkOS, Next.js) Mocked (everything)
Purpose Verify integration Verify logic

Answer to Question: Should We Migrate?

NO - These should remain separate.

Keep in tests/integration/setup/:

factories/database-fixtures.ts - Creates REAL database records ✅ factories/workos-events.ts - WorkOS event data for integration tests
mocks.ts - Mocks external APIs for integration tests ✅ fixtures.ts - Test data helpers ✅ utilities/ - Database cleanup and query helpers

Why keep them separate:

  1. Different purposes - Integration tests verify workflows, unit tests verify logic
  2. Different dependencies - Integration needs real DB, unit needs mocks
  3. Already well-organized - Integration test infrastructure is complete and documented
  4. No duplication - They serve different testing needs

Add to tests/mocks/ (Unit Tests Only):

🆕 auth/ - Auth service mocks for unit tests 🆕 services/ - Service layer mocks for unit tests 🆕 models/ - Model fixtures for unit tests ✅ db/ - Database client mocks for unit tests (already complete)


When to Use Which

Use Integration Test Infrastructure When:

  • Testing complete user workflows
  • Verifying database schema/constraints
  • Testing graph traversals and permissions
  • Validating API endpoints end-to-end
  • Testing webhook handlers
  • Need real database behavior

Use Unit Test Mocks When:

  • Testing individual functions
  • Testing class methods in isolation
  • Verifying business logic
  • Testing error handling
  • Fast feedback during development
  • No database dependencies needed

Example Scenarios

Scenario 1: Testing Permission Check Logic

Unit Test (use tests/mocks/):

import { createPoolClientMock } from '@tests/mocks';

// Test the checkPermission FUNCTION logic
it('should return true for valid permission', async () => {
  const client = createPoolClientMock({
    query: vi.fn().mockResolvedValue({
      rows: [{ has_permission: true }]
    })
  });
  
  const result = await checkPermission(client, 'user', 'resource', 'read');
  expect(result).toBe(true);
});

Integration Test (use tests/integration/setup/):

import { setupTestWorkspace } from '../setup/factories/database-fixtures';

// Test the COMPLETE permission flow with real database
it('should grant and verify permissions end-to-end', async () => {
  const workspace = await setupTestWorkspace(client, {
    resourceCount: 1,
    resourcePermission: 'read'
  });
  
  // This actually queries the database
  const result = await checkPermission(
    client,
    workspace.user.nodeId,
    workspace.resources[0].nodeId,
    'read'
  );
  expect(result).toBe(true);
});

Scenario 2: Testing WorkOS Integration

Unit Test (use tests/mocks/):

import { createWorkOSClientMock } from '@tests/mocks'; // To be implemented

// Test service logic that calls WorkOS
it('should handle WorkOS user fetch', async () => {
  const workos = createWorkOSClientMock({
    userManagement: {
      getUser: vi.fn().mockResolvedValue({
        id: 'user_123',
        email: 'test@example.com'
      })
    }
  });
  
  const service = new UserService(workos);
  const user = await service.fetchUser('user_123');
  expect(user.email).toBe('test@example.com');
});

Integration Test (use tests/integration/setup/):

import '../setup/mocks'; // Mocks WorkOS API
import { createWorkOSWebhookEvent } from '../setup/factories/workos-events';

// Test complete webhook handling flow
it('should process user.created webhook', async () => {
  const event = createWorkOSWebhookEvent('user.created', {
    user: {
      id: 'user_123',
      email: 'test@example.com'
    }
  });
  
  // This creates real database records
  await handleWebhook(event);
  
  // Verify in database
  const user = await client.query(
    'SELECT * FROM users WHERE external_id = $1',
    ['user_123']
  );
  expect(user.rows).toHaveLength(1);
});

Summary

DO NOT MIGRATE integration test infrastructure to tests/mocks/.

The two systems serve different purposes:

  • Integration tests verify complete workflows with real database
  • Unit tests verify isolated logic with mocks

Keep them separate and continue building out tests/mocks/ for unit testing needs only.


Next Steps

Proceed with implementing Phase 5-8 for unit test mocks only:

  • ✅ Phase 5: Auth mocks for unit tests
  • ✅ Phase 6: WorkOS mocks for unit tests (different from integration!)
  • ✅ Phase 7: Service/model mocks for unit tests
  • ✅ Phase 8: Documentation

Integration test infrastructure is complete and should remain as-is in tests/integration/setup/.