Data Layer Test Strategy

Overview

กลยุทธ์การทดสอบสำหรับ Data Layer ที่ออกแบบตาม Action-Based Structure โดยเน้นการทดสอบที่มีประสิทธิภาพ ครอบคลุม และง่ายต่อการบำรุงรักษา

Core Testing Philosophy

Data Layer Testing Focus

Data Layer มุ่งเน้นการทดสอบ ความถูกต้องของการเข้าถึงข้อมูล ไม่ใช่ business functionality:

  • Database Operations: ทดสอบ CRUD operations
  • Data Transformations: ทดสอบการแปลงข้อมูล
  • Error Handling: ทดสอบการจัดการข้อผิดพลาด
  • Repository Interface Compliance: ทดสอบการ implement interface

Testing Principles

  1. Fast Feedback: Unit tests ต้องเร็ว เพื่อ developer experience
  2. Isolated Testing: แต่ละ test เป็นอิสระ ไม่ depend กัน
  3. Realistic Data: Integration tests ใช้ข้อมูลจริง
  4. Maintainable Tests: Test code ต้องง่ายต่อการบำรุงรักษา

Test Levels & Scope

1. Unit Tests

  • Purpose: ทดสอบ logic แต่ละ function แยกกัน
  • Data Management: Fixture Objects + Mock
  • Database: No Database (Mocked PrismaClient)
  • Speed: Very Fast (< 100ms per test)
  • Isolation: Perfect (no external dependencies)

2. Integration Tests

  • Purpose: ทดสอบการทำงานร่วมกันของ components กับ database จริง
  • Data Management: SQL Files + Shared Docker Container
  • Database: Real Database หรือ Docker Container
  • Speed: Medium (1-5 seconds per test)
  • Isolation: Data cleanup between tests

3. System Tests

  • Purpose: ทดสอบระบบทั้งหมดในสถานการณ์ใกล้เคียง production
  • Data Management: SQL Files + Docker Containers (per suite)
  • Database: Docker Containers (isolated per test suite)
  • Speed: Slow (10+ seconds per test)
  • Isolation: Complete (separate container per suite)

Test Data Management

Understanding Test Data Types

Seed Data

  • วัตถุประสงค์: ข้อมูลพื้นฐานที่จำเป็นสำหรับระบบทำงาน
  • ลักษณะ: ข้อมูล Master Data, Reference Data ที่ไม่เปลี่ยนแปลงบ่อย
  • การใช้งาน: Load ครั้งเดียวตอน setup database
  • ตัวอย่าง:
    • Country, Province, District
    • User Roles, Permissions
    • Application Status Types
  • Location: shared-test-data/master/ และ api-name/test-data/seed/
-- seed-data.sql
INSERT INTO COUNTRY (ID, NAME_EN, NAME_TH) VALUES 
('TH', 'Thailand', 'ประเทศไทย'),
('US', 'United States', 'สหรัฐอเมริกา');

INSERT INTO ROLES (ID, NAME, PERMISSIONS) VALUES 
('ADMIN', 'Administrator', '["CREATE", "READ", "UPDATE", "DELETE"]'),
('USER', 'Regular User', '["READ"]');

Load Data (Test Data)

  • วัตถุประสงค์: ข้อมูลจำลองสำหรับ test case เฉพาะ
  • ลักษณะ: ข้อมูลธุรกิจที่เปลี่ยนแปลงตาม test scenario
  • การใช้งาน: Load ก่อน test แต่ละ case, Clear หลัง test
  • ตัวอย่าง:
    • Application records
    • User transactions
    • Business data for specific scenarios
  • Location: action/test-data/actions/test-case/

Fixture

  • วัตถุประสงค์: ข้อมูลสำเร็จรูปสำหรับ test แต่ละชุด
  • ลักษณะ: ข้อมูลที่จัดเตรียมไว้ล่วงหน้า, อาจเป็น JSON, Object, หรือ SQL
  • การใช้งาน: ใช้ซ้ำได้หลาย test, มักเป็น static data
  • ตัวอย่าง:
// user.fixture.ts
export const testUsers = [
  { id: "user1", name: "John Doe", role: "admin" },
  { id: "user2", name: "Jane Smith", role: "user" }
];

Test Data Hierarchy

LevelLocationPurposeExamplesUsage
Globalshared-test-data/Cross-API master dataCountries, RolesLoad once per test run
APIapi-name/test-data/seed/API-specific master dataDocument Types, Process ActivitiesLoad once per API test suite
Actionaction/test-data/actions/Business test scenariosApplication states, User flowsLoad per test case
Unit__tests__/fixtures/Test objectsMock data objectsImport in test files

Database Testing Strategy

Database Choices by Test Level

Unit Tests: No Database

// Mock PrismaClient for unit tests
import { mockDeep } from 'jest-mock-extended';
import type { PrismaClient } from '@prisma/client';

const mockPrisma = mockDeep<PrismaClient>();
mockPrisma.aPPLICATION.findUnique.mockResolvedValue(testApplication);

Integration Tests: Real Database or Shared Container

Option A: Real Database

// Use existing database with test schema
const client = new PrismaClient({
  datasources: {
    db: { url: process.env.TEST_DATABASE_URL }
  }
});

Option B: Shared Docker Container

// Single container for all integration tests
const container = await new GenericContainer('postgres:15')
  .withName('integration-postgres')
  .withEnvironment({
    POSTGRES_DB: 'integration_db',
    POSTGRES_USER: 'test',
    POSTGRES_PASSWORD: 'test'
  })
  .start();

System Tests: Isolated Containers

// Separate container per test suite
const container = await new GenericContainer('postgres:15')
  .withName(`system-test-${suiteName}-${Date.now()}`)
  .withEnvironment({
    POSTGRES_DB: `system_${suiteName}`,
    POSTGRES_USER: 'test',
    POSTGRES_PASSWORD: 'test'
  })
  .start();

Data Loading Strategy

Test Data Loading Sequence

// Global Setup (once per test run)
beforeAll(async () => {
  // 1. Load global master data
  await TestDataManager.loadGlobalData();
  
  // 2. Load API-specific seed data  
  await TestDataManager.loadAPISeed('document-process-api');
});

// Test Case Setup (per test)
beforeEach(async () => {
  // 3. Load action-specific test data
  await TestDataManager.loadActionData('return-request-lv40', 'valid-request');
});

// Test Case Cleanup (per test)
afterEach(async () => {
  // 4. Clean test data (keep master data)
  await TestDataManager.cleanTestData();
});

Mock Strategy & Fixtures

Fixture Organization

Fixture Structure

action-folder/
├── __tests__/
│   ├── fixtures/                 # Unit test fixtures
│   │   ├── user.fixture.ts       # User test data objects
│   │   ├── application.fixture.ts # Application test data
│   │   ├── packsize.fixture.ts   # Packsize test data
│   │   └── index.ts              # Central fixture export
│   ├── unit/                     # Unit tests using fixtures
│   ├── integration/              # Integration tests using SQL
│   └── system/                   # System tests using SQL

Fixture Implementation

// fixtures/user.fixture.ts
export const testUsers = {
  validUser: {
    id: "test-user-001",
    username: "testuser",
    role: "USER",
    createdAt: new Date("2024-01-01")
  },
  adminUser: {
    id: "admin-001", 
    username: "admin",
    role: "ADMIN",
    createdAt: new Date("2024-01-01")
  }
};

// fixtures/application.fixture.ts
export const testApplications = {
  pendingApplication: {
    id: "app-001",
    applicationNo: "APP2024001",
    status: "PENDING",
    currentProcessActivityId: "ACT_004",
    createdBy: "test-user-001"
  },
  completedApplication: {
    id: "app-002", 
    applicationNo: "APP2024002",
    status: "COMPLETED",
    currentProcessActivityId: "ACT_004",
    createdBy: "test-user-001"
  }
};

// fixtures/index.ts - Central export
export * from './user.fixture';
export * from './application.fixture';
export * from './packsize.fixture';

// Complex fixture with relationships
export const completeApplicationFixture = {
  application: testApplications.pendingApplication,
  packsizes: [
    { id: "pack-001", packsizeId: "SIZE_1KG" },
    { id: "pack-002", packsizeId: "SIZE_5KG" }
  ],
  attachments: [
    { id: "att-001", fileName: "cert.pdf", filePath: "/uploads/cert.pdf" }
  ],
  user: testUsers.validUser
};

Mock Strategy

PrismaClient Mocking

// Unit test with PrismaClient mock
import { mockDeep } from 'jest-mock-extended';
import type { PrismaClient } from '@prisma/client';
import { testApplications } from '../fixtures';

describe('getApplicationDAF', () => {
  let mockPrisma: MockProxy<PrismaClient>;
  
  beforeEach(() => {
    mockPrisma = mockDeep<PrismaClient>();
  });

  it('should return application by id', async () => {
    // Arrange
    const expectedApp = testApplications.pendingApplication;
    mockPrisma.aPPLICATION.findUnique.mockResolvedValue(expectedApp);
    
    // Act
    const result = await getApplicationDAF(mockPrisma, "app-001");
    
    // Assert
    expect(result).toEqual(expectedApp);
    expect(mockPrisma.aPPLICATION.findUnique).toHaveBeenCalledWith({
      where: { ID: "app-001" }
    });
  });
});

External Service Mocking

// Mock telemetry service
const mockTelemetryService = {
  getActiveTelemetry: jest.fn().mockReturnValue({
    telemetryLogger: {
      info: jest.fn(),
      error: jest.fn(),
      warn: jest.fn()
    }
  }),
  executeFunctionWithDbSpan: jest.fn().mockImplementation(
    async (context, fn, input) => await fn(input)
  )
};

Test Structure & Organization

Project Structure with Tests

feedos-frgm-data/store-prisma/src/
├── shared-test-data/                     # 🌍 Global master data
│   ├── master/
│   │   ├── countries.sql
│   │   ├── roles.sql
│   │   └── permissions.sql
│   └── base-users.sql
├── document-process-api/
│   ├── test-data/                        # 🏢 API-Level test data  
│   │   └── seed/
│   │       ├── document-types.sql
│   │       ├── process-activities.sql
│   │       └── application-statuses.sql
│   ├── test-setup/                       # 🔧 API-Level test infrastructure
│   │   ├── integration-setup.ts          # Docker container management
│   │   ├── test-data-manager.ts          # SQL loading utilities
│   │   └── jest.config.integration.js    # Integration test config
│   ├── command/
│   │   ├── return-request-lv40/          # 🎯 Action Folder
│   │   │   ├── repository.ts
│   │   │   ├── return-request/           # 📁 Method Logic
│   │   │   ├── __tests__/                # 🧪 Action Tests
│   │   │   │   ├── fixtures/             # Unit test data objects
│   │   │   │   │   ├── user.fixture.ts
│   │   │   │   │   ├── application.fixture.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── unit/                 # Unit tests (Mock + Fixture)
│   │   │   │   │   ├── repository.test.ts
│   │   │   │   │   ├── task.test.ts
│   │   │   │   │   ├── db.logic.test.ts
│   │   │   │   │   └── flows/
│   │   │   │   ├── integration/          # Integration tests (SQL + Docker)
│   │   │   │   │   ├── return-request.integration.test.ts
│   │   │   │   │   └── check-status.integration.test.ts
│   │   │   │   └── system/               # System tests (SQL + Containers)
│   │   │   │       └── return-request.system.test.ts
│   │   │   ├── test-data/                # 🗃️ Action-specific SQL data
│   │   │   │   └── actions/
│   │   │   │       └── return-request-lv40/
│   │   │   │           ├── valid-request/
│   │   │   │           │   ├── setup.sql
│   │   │   │           │   └── teardown.sql
│   │   │   │           ├── invalid-status/
│   │   │   │           │   ├── setup.sql
│   │   │   │           │   └── teardown.sql
│   │   │   │           └── missing-attachments/
│   │   │   │               ├── setup.sql
│   │   │   │               └── teardown.sql
│   │   │   └── index.ts
│   │   └── [other-actions]/
│   └── query/
│       └── [query-actions]/

Test Infrastructure

Test Data Manager

// test-setup/test-data-manager.ts
export class TestDataManager {
  private static client: PrismaClient;
  
  // Load global master data (once per test run)
  static async loadGlobalData() {
    console.log('Loading global master data...');
    await this.executeSQL('shared-test-data/master/countries.sql');
    await this.executeSQL('shared-test-data/master/roles.sql');
    await this.executeSQL('shared-test-data/base-users.sql');
  }
  
  // Load API-specific seed data (once per API test suite)
  static async loadAPISeed(apiName: string) {
    console.log(`Loading ${apiName} seed data...`);
    await this.executeSQL(`${apiName}/test-data/seed/document-types.sql`);
    await this.executeSQL(`${apiName}/test-data/seed/process-activities.sql`);
  }
  
  // Load action-specific test data (per test case)
  static async loadActionData(actionName: string, testCase: string) {
    console.log(`Loading ${actionName}/${testCase} test data...`);
    const basePath = `document-process-api/command/${actionName}/test-data/actions/${actionName}`;
    await this.executeSQL(`${basePath}/${testCase}/setup.sql`);
  }
  
  // Clean test data (keep master data)
  static async cleanTestData() {
    await this.executeSQL(`
      DELETE FROM APPLICATION_HISTORY WHERE APPLICATION_ID LIKE 'test-%';
      DELETE FROM APPLICATION_ATTACHMENT WHERE APPLICATION_ID LIKE 'test-%';
      DELETE FROM APPLICATION_MAPPING_PACKSIZE WHERE APPLICATION_ID LIKE 'test-%';
      DELETE FROM APPLICATION WHERE ID LIKE 'test-%';
    `);
  }
  
  private static async executeSQL(filePath: string) {
    const sql = fs.readFileSync(path.join(__dirname, '..', filePath), 'utf8');
    await this.client.$executeRawUnsafe(sql);
  }
}

Integration Test Setup

// test-setup/integration-setup.ts
export class IntegrationTestManager {
  private static container: StartedTestContainer;
  private static client: PrismaClient;
  
  static async setupSharedContainer() {
    if (!this.container) {
      console.log('Starting shared PostgreSQL container...');
      this.container = await new GenericContainer('postgres:15')
        .withName('integration-postgres')
        .withEnvironment({
          POSTGRES_DB: 'integration_test_db',
          POSTGRES_USER: 'test',
          POSTGRES_PASSWORD: 'test'
        })
        .withExposedPorts(5432)
        .start();
        
      this.client = new PrismaClient({
        datasources: {
          db: {
            url: `postgresql://test:test@localhost:${this.container.getMappedPort(5432)}/integration_test_db`
          }
        }
      });
      
      await this.runMigrations();
      await TestDataManager.loadGlobalData();
    }
  }
  
  static getClient(): PrismaClient {
    return this.client;
  }
  
  static async cleanup() {
    if (this.client) {
      await this.client.$disconnect();
    }
    if (this.container) {
      await this.container.stop();
    }
  }
}

Testing Examples

Unit Test Example

// __tests__/unit/db.logic.test.ts
import { mockDeep } from 'jest-mock-extended';
import type { PrismaClient } from '@prisma/client';
import { findHistoryDetailDAF } from '../../return-request/db.logic';
import { testApplications } from '../fixtures';

describe('findHistoryDetailDAF', () => {
  let mockPrisma: MockProxy<PrismaClient>;
  
  beforeEach(() => {
    mockPrisma = mockDeep<PrismaClient>();
  });

  it('should return application history detail', async () => {
    // Arrange
    const expectedResult = {
      APPLICATION: testApplications.pendingApplication,
      APPLICATION_MAPPING_PACKSIZE: [
        { PACKSIZE_ID: "SIZE_1KG" },
        { PACKSIZE_ID: "SIZE_5KG" }
      ],
      APPLICATION_ATTACHMENT: [
        { FILE_NAME: "cert.pdf", FILE_PATH: "/uploads/cert.pdf", REGISTRATION_FLAG: true }
      ]
    };
    
    mockPrisma.aPPLICATION.findUnique.mockResolvedValue(expectedResult);
    
    // Act
    const result = await findHistoryDetailDAF({
      propContext: { client: mockPrisma },
      input: { id: "app-001" }
    });
    
    // Assert
    expect(result.isRight()).toBe(true);
    expect(result.value).toEqual(expectedResult);
  });
  
  it('should handle application not found', async () => {
    // Arrange
    mockPrisma.aPPLICATION.findUnique.mockResolvedValue(null);
    
    // Act
    const result = await findHistoryDetailDAF({
      propContext: { client: mockPrisma },
      input: { id: "non-existent" }
    });
    
    // Assert
    expect(result.isLeft()).toBe(true);
    expect(result.value.message).toContain('Application not found');
  });
});

Integration Test Example

// __tests__/integration/return-request.integration.test.ts
import { IntegrationTestManager, TestDataManager } from '../../test-setup';
import { ReturnRequestLv40V1Repo } from '../repository';

describe('Return Request Integration Tests', () => {
  let testClient: PrismaClient;
  let repository: ReturnRequestLv40V1Repo;
  
  beforeAll(async () => {
    await IntegrationTestManager.setupSharedContainer();
    testClient = IntegrationTestManager.getClient();
    repository = new ReturnRequestLv40V1Repo(testClient, mockTelemetryService);
  });
  
  afterAll(async () => {
    await IntegrationTestManager.cleanup();
  });

  describe('Valid Return Request', () => {
    beforeEach(async () => {
      await TestDataManager.loadActionData('return-request-lv40', 'valid-request');
    });
    
    afterEach(async () => {
      await TestDataManager.cleanTestData();
    });
    
    it('should process return request successfully', async () => {
      // Act
      const result = await repository.returnRequest(mockContext, {
        id: 'test-app-001',
        userId: 'test-user-001'
      });
      
      // Assert
      expect(result.isOk()).toBe(true);
      expect(result.value.id).toBe('test-app-001');
    });
  });

  describe('Invalid Application Status', () => {
    beforeEach(async () => {
      await TestDataManager.loadActionData('return-request-lv40', 'invalid-status');
    });
    
    afterEach(async () => {
      await TestDataManager.cleanTestData();
    });
    
    it('should reject request with invalid status', async () => {
      // Act
      const result = await repository.returnRequest(mockContext, {
        id: 'test-app-invalid-001',
        userId: 'test-user-001'
      });
      
      // Assert
      expect(result.isErr()).toBe(true);
      expect(result.error.message).toContain('Invalid application status');
    });
  });
});

System Test Example

// __tests__/system/return-request.system.test.ts
describe('Return Request System Tests', () => {
  let testContainer: StartedTestContainer;
  let testClient: PrismaClient;
  
  beforeAll(async () => {
    // Create isolated container for this test suite
    testContainer = await new GenericContainer('postgres:15')
      .withName(`system-return-request-${Date.now()}`)
      .withEnvironment({
        POSTGRES_DB: 'system_return_request',
        POSTGRES_USER: 'test',
        POSTGRES_PASSWORD: 'test'
      })
      .start();
      
    testClient = new PrismaClient({
      datasources: {
        db: {
          url: `postgresql://test:test@localhost:${testContainer.getMappedPort(5432)}/system_return_request`
        }
      }
    });
    
    await runMigrations(testClient);
    await loadSystemTestData(testClient);
  });
  
  afterAll(async () => {
    await testClient.$disconnect();
    await testContainer.stop();
  });
  
  it('should handle complete return request workflow', async () => {
    // Test complete system workflow including:
    // - Multiple database operations
    // - Transaction rollback scenarios  
    // - Error propagation
    // - Performance characteristics
  });
});

DAF (Database Access Function) Testing

DAF Unit Testing Patterns

Input/Output Testing

describe('getApplicationDAF', () => {
  it('should return application with correct structure', async () => {
    // Arrange
    const mockResult = testApplications.pendingApplication;
    mockPrisma.aPPLICATION.findUnique.mockResolvedValue(mockResult);
    
    // Act
    const result = await getApplicationDAF(mockPrisma, "app-001");
    
    // Assert
    expect(result).toMatchObject({
      ID: expect.any(String),
      APPLICATION_NO: expect.any(String),
      APPLICATION_DOC_STATUS: expect.any(String),
      CURRENT_PROCESS_ACTIVITY_ID: expect.any(String)
    });
  });
});

Database Query Testing

describe('findApplicationsByStatusDAF', () => {
  it('should call prisma with correct query parameters', async () => {
    // Arrange
    const status = 'PENDING';
    const limit = 10;
    
    // Act
    await findApplicationsByStatusDAF(mockPrisma, status, limit);
    
    // Assert
    expect(mockPrisma.aPPLICATION.findMany).toHaveBeenCalledWith({
      where: { APPLICATION_DOC_STATUS: status },
      take: limit,
      orderBy: { CREATED_AT: 'desc' }
    });
  });
});

Error Handling Testing

describe('updateApplicationStatusDAF', () => {
  it('should handle database constraint violations', async () => {
    // Arrange
    const dbError = new Error('Unique constraint violation');
    mockPrisma.aPPLICATION.update.mockRejectedValue(dbError);
    
    // Act & Assert
    await expect(updateApplicationStatusDAF(mockPrisma, {
      id: "app-001",
      status: "INVALID_STATUS"
    })).rejects.toThrow('Unique constraint violation');
  });
});

Flow Function Testing

Flow Functions ควรทดสอบที่ระดับ Business Orchestration โดย Mock DAF Functions แทนการ Mock PrismaClient เพื่อ:

  • เน้นทดสอบ workflow coordination
  • ง่ายต่อการ mock และ maintain
  • Test ที่ระดับ business logic ไม่ใช่ database implementation

Flow Function Unit Test with DAF Mocking

// __tests__/unit/flows/getAllActivity.test.ts
import { getAllActivityFlow } from '../../../return-request/flows/getAllActivity.flow';
import { findActivityDAF, findManyProcessActivityDAF } from '../../../return-request/db.logic';
import { maptToAllActityItem } from '../../../return-request/data.logic';

// Mock DAF functions instead of PrismaClient
jest.mock('../../../return-request/db.logic', () => ({
  findActivityDAF: jest.fn(),
  findManyProcessActivityDAF: jest.fn()
}));

jest.mock('../../../return-request/data.logic', () => ({
  maptToAllActityItem: jest.fn()
}));

const mockFindActivityDAF = findActivityDAF as jest.MockedFunction<typeof findActivityDAF>;
const mockFindManyProcessActivityDAF = findManyProcessActivityDAF as jest.MockedFunction<typeof findManyProcessActivityDAF>;
const mockMaptToAllActityItem = maptToAllActityItem as jest.MockedFunction<typeof maptToAllActityItem>;

describe('getAllActivityFlow', () => {
  let mockTelemetryService: any;
  let mockContext: any;
  
  beforeEach(() => {
    jest.clearAllMocks();
    
    mockTelemetryService = {
      executeFunctionWithDbSpan: jest.fn().mockImplementation(
        async (context, fn, input) => await fn(input)
      )
    };
    
    mockContext = { requestId: 'test-123' };
  });
  
  it('should orchestrate activity data retrieval successfully', async () => {
    // Arrange
    const mockCurrentActivity = { 
      ID: "act-001", 
      PROCESS_ID: "proc-001",
      ACTIVITY_LEVEL: "LV40" 
    };
    
    const mockProcessActivities = [
      { ID: "act-001", ACTIVITY_LEVEL: "LV10" },
      { ID: "act-002", ACTIVITY_LEVEL: "LV20" },
      { ID: "act-003", ACTIVITY_LEVEL: "LV40" }
    ];
    
    const mockTransformedResult = {
      allAct: mockProcessActivities,
      CURRENT_PROCESS_ACTIVITY_ID: "act-001"
    };
    
    // Mock DAF function responses (business level)
    mockFindActivityDAF.mockResolvedValue(right(mockCurrentActivity));
    mockFindManyProcessActivityDAF.mockResolvedValue(right(mockProcessActivities));
    mockMaptToAllActityItem.mockReturnValue(mockTransformedResult);
    
    // Act - Test orchestration flow
    const result = await getAllActivityFlow({
      client: {} as PrismaClient, // Don't need real client since we mock DAFs
      telemetryService: mockTelemetryService,
      context: mockContext,
      applicationId: "app-001"
    });
    
    // Assert - Verify orchestration logic
    expect(result.isRight()).toBe(true);
    expect(result.value).toEqual(mockTransformedResult);
    
    // Verify DAF function calls (business operations)
    expect(mockFindActivityDAF).toHaveBeenCalledWith({
      propContext: { client: {} },
      input: { appId: "app-001" }
    });
    
    expect(mockFindManyProcessActivityDAF).toHaveBeenCalledWith({
      propContext: { client: {} },
      input: { processId: "proc-001" }
    });
    
    // Verify data transformation
    expect(mockMaptToAllActityItem).toHaveBeenCalledWith(
      mockProcessActivities, 
      mockCurrentActivity
    );
    
    // Verify telemetry service usage
    expect(mockTelemetryService.executeFunctionWithDbSpan).toHaveBeenCalledTimes(2);
  });
  
  it('should handle current activity fetch failure', async () => {
    // Arrange
    const mockError = new BaseFailure('Activity not found');
    mockFindActivityDAF.mockResolvedValue(left(mockError));
    
    // Act
    const result = await getAllActivityFlow({
      client: {} as PrismaClient,
      telemetryService: mockTelemetryService,
      context: mockContext,
      applicationId: "non-existent"
    });
    
    // Assert - Test error handling flow
    expect(result.isLeft()).toBe(true);
    expect(result.value).toBe(mockError);
    
    // Should not call subsequent operations
    expect(mockFindManyProcessActivityDAF).not.toHaveBeenCalled();
    expect(mockMaptToAllActityItem).not.toHaveBeenCalled();
  });
});

Testing Strategy Comparison

Test TargetMock LevelTest FocusBenefits
DAF FunctionsMock PrismaClientDatabase operationsTest SQL queries, database logic
Flow FunctionsMock DAF FunctionsBusiness orchestrationTest workflow, coordination, error handling
Task FunctionsMock Flow + DAFHigh-level coordinationTest overall business logic flow

Best Practices & Guidelines

Test Organization

  1. Clear Test Naming: ใช้ describe/it ที่สื่อความหมาย
  2. AAA Pattern: Arrange, Act, Assert ในทุก test
  3. Single Responsibility: แต่ละ test ทดสอบสิ่งเดียว
  4. Independent Tests: test ไม่ควร depend กัน

Data Management

  1. Predictable IDs: ใช้ ID ที่กำหนดไว้ล่วงหน้า (เช่น test-app-001)
  2. Clean State: reset data หลังแต่ละ test
  3. Minimal Data: ใช้ข้อมูลน้อยที่สุดที่จำเป็น
  4. Realistic Data: ข้อมูลควรสมจริง แต่ไม่ซับซ้อน

Mock Guidelines

  1. Mock External Dependencies: database, external services
  2. Don’t Mock What You Own: ไม่ mock code ที่เขียนเอง
  3. Verify Interactions: ตรวจสอบการเรียกใช้ mock
  4. Reset Mocks: reset mock หลังแต่ละ test

Performance Considerations

  1. Fast Unit Tests: Unit tests ต้องเร็ว (< 100ms)
  2. Reasonable Integration Tests: Integration tests ควรเร็วพอสมควร (< 5s)
  3. Parallel Execution: run tests แบบ parallel เมื่อเป็นไปได้
  4. Resource Cleanup: clean up resources หลัง tests

Jest Configuration

Unit Test Configuration

// jest.config.unit.js
module.exports = {
  displayName: 'Unit Tests',
  testMatch: ['**/__tests__/unit/**/*.test.ts'],
  setupFilesAfterEnv: ['<rootDir>/test-setup/unit-setup.ts'],
  clearMocks: true,
  resetMocks: true,
  restoreMocks: true
};

Integration Test Configuration

// jest.config.integration.js
module.exports = {
  displayName: 'Integration Tests',
  testMatch: ['**/__tests__/integration/**/*.test.ts'],
  globalSetup: '<rootDir>/test-setup/integration-global-setup.ts',
  globalTeardown: '<rootDir>/test-setup/integration-global-teardown.ts',
  setupFilesAfterEnv: ['<rootDir>/test-setup/integration-setup.ts'],
  testTimeout: 30000 // 30 seconds for integration tests
};

System Test Configuration

// jest.config.system.js
module.exports = {
  displayName: 'System Tests',
  testMatch: ['**/__tests__/system/**/*.test.ts'],
  setupFilesAfterEnv: ['<rootDir>/test-setup/system-setup.ts'],
  testTimeout: 60000, // 60 seconds for system tests
  maxConcurrency: 2   // Limit concurrent system tests
};

Summary

Test Strategy Matrix

Test LevelData SourceDatabaseSpeedIsolationUse Case
UnitFixture ObjectsMockVery FastPerfectLogic validation
IntegrationSQL FilesReal DB/DockerMediumData cleanupComponent interaction
SystemSQL FilesDocker per suiteSlowCompleteEnd-to-end workflow

Key Benefits

  1. Fast Developer Feedback: Unit tests ให้ feedback เร็ว
  2. Realistic Testing: Integration tests ใช้ database จริง
  3. Complete Isolation: System tests มี environment แยก
  4. Maintainable: Test code ง่ายต่อการบำรุงรักษา
  5. Scalable: รองรับ team และ codebase ที่เติบโต

Implementation Priorities

  1. Start with Unit Tests: สร้าง unit tests ก่อน
  2. Add Integration Tests: เพิ่ม integration tests สำหรับ critical paths
  3. System Tests as Needed: เพิ่ม system tests สำหรับ complex workflows
  4. Maintain Test Data: จัดการ test data อย่างเป็นระบบ
  5. Monitor Performance: ติดตาม performance ของ tests

Strategy นี้ช่วยให้ Data Layer มีการทดสอบที่ครอบคลุม เชื่อถือได้ และง่ายต่อการบำรุงรักษา โดยเน้นที่การทดสอบความถูกต้องของการเข้าถึงข้อมูลและการทำงานของ repository pattern ตาม Action-Based Structure Design