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
- Fast Feedback: Unit tests ต้องเร็ว เพื่อ developer experience
- Isolated Testing: แต่ละ test เป็นอิสระ ไม่ depend กัน
- Realistic Data: Integration tests ใช้ข้อมูลจริง
- 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
| Level | Location | Purpose | Examples | Usage |
|---|---|---|---|---|
| Global | shared-test-data/ | Cross-API master data | Countries, Roles | Load once per test run |
| API | api-name/test-data/seed/ | API-specific master data | Document Types, Process Activities | Load once per API test suite |
| Action | action/test-data/actions/ | Business test scenarios | Application states, User flows | Load per test case |
| Unit | __tests__/fixtures/ | Test objects | Mock data objects | Import 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 Target | Mock Level | Test Focus | Benefits |
|---|---|---|---|
| DAF Functions | Mock PrismaClient | Database operations | Test SQL queries, database logic |
| Flow Functions | Mock DAF Functions | Business orchestration | Test workflow, coordination, error handling |
| Task Functions | Mock Flow + DAF | High-level coordination | Test overall business logic flow |
Best Practices & Guidelines
Test Organization
- Clear Test Naming: ใช้ describe/it ที่สื่อความหมาย
- AAA Pattern: Arrange, Act, Assert ในทุก test
- Single Responsibility: แต่ละ test ทดสอบสิ่งเดียว
- Independent Tests: test ไม่ควร depend กัน
Data Management
- Predictable IDs: ใช้ ID ที่กำหนดไว้ล่วงหน้า (เช่น test-app-001)
- Clean State: reset data หลังแต่ละ test
- Minimal Data: ใช้ข้อมูลน้อยที่สุดที่จำเป็น
- Realistic Data: ข้อมูลควรสมจริง แต่ไม่ซับซ้อน
Mock Guidelines
- Mock External Dependencies: database, external services
- Don’t Mock What You Own: ไม่ mock code ที่เขียนเอง
- Verify Interactions: ตรวจสอบการเรียกใช้ mock
- Reset Mocks: reset mock หลังแต่ละ test
Performance Considerations
- Fast Unit Tests: Unit tests ต้องเร็ว (< 100ms)
- Reasonable Integration Tests: Integration tests ควรเร็วพอสมควร (< 5s)
- Parallel Execution: run tests แบบ parallel เมื่อเป็นไปได้
- 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 Level | Data Source | Database | Speed | Isolation | Use Case |
|---|---|---|---|---|---|
| Unit | Fixture Objects | Mock | Very Fast | Perfect | Logic validation |
| Integration | SQL Files | Real DB/Docker | Medium | Data cleanup | Component interaction |
| System | SQL Files | Docker per suite | Slow | Complete | End-to-end workflow |
Key Benefits
- Fast Developer Feedback: Unit tests ให้ feedback เร็ว
- Realistic Testing: Integration tests ใช้ database จริง
- Complete Isolation: System tests มี environment แยก
- Maintainable: Test code ง่ายต่อการบำรุงรักษา
- Scalable: รองรับ team และ codebase ที่เติบโต
Implementation Priorities
- Start with Unit Tests: สร้าง unit tests ก่อน
- Add Integration Tests: เพิ่ม integration tests สำหรับ critical paths
- System Tests as Needed: เพิ่ม system tests สำหรับ complex workflows
- Maintain Test Data: จัดการ test data อย่างเป็นระบบ
- Monitor Performance: ติดตาม performance ของ tests
Strategy นี้ช่วยให้ Data Layer มีการทดสอบที่ครอบคลุม เชื่อถือได้ และง่ายต่อการบำรุงรักษา โดยเน้นที่การทดสอบความถูกต้องของการเข้าถึงข้อมูลและการทำงานของ repository pattern ตาม Action-Based Structure Design