# Testing Best Practices for Jomblo E-commerce Platform

This guide establishes patterns and conventions for writing robust, maintainable tests that verify behavior rather than implementation details.

## Core Testing Principles

### 1. Test Behavior, Not Implementation
✅ **Good**: Test that clicking "Add to Cart" increases cart count
❌ **Bad**: Test that `handleAddToCart` function is called

### 2. Use Consistent Mock Setup
Always use the provided mock utilities to ensure test isolation and consistency.

### 3. Focus on Business Logic
Tests should verify that business rules are enforced correctly.

### 4. Test Edge Cases and Error Conditions
Verify how the system handles invalid input, network failures, and unexpected states.

## Test Structure

### Basic Test Structure
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { TestEnvironment, AssertionHelpers, MockFactory } from '@/tests/utils'

describe('Feature Name', () => {
  beforeEach(() => {
    TestEnvironment.cleanup()
  })

  describe('when user is authenticated', () => {
    beforeEach(() => {
      TestEnvironment.setupComplete(MockFactory.session(MockFactory.user()))
    })

    it('should perform expected behavior', async () => {
      // Arrange
      const { mockClient } = TestEnvironment.setupComplete()
      
      // Act
      const result = await functionUnderTest()
      
      // Assert
      AssertionHelpers.assertApiResponseStructure(result, 'success', ['id', 'name'])
    })
  })
})
```

## Mock Usage Guidelines

### Database Mocks
```typescript
// Good: Use MockSetup for consistent database mocking
const { mockClient } = MockSetup.setupDatabaseMocks()
mockClient.query.mockResolvedValue(DatabaseMockFactory.singleRow(expectedData))

// Verify transaction patterns
AssertionHelpers.assertTransactionPattern(mockClient, ['INSERT INTO products', 'COMMIT'])
```

### Authentication Mocks
```typescript
// Good: Use predefined session factories
const userSession = MockFactory.session(MockFactory.user())
const adminSession = MockFactory.session(MockFactory.admin())

TestEnvironment.setupComplete(userSession)
```

### API Mocks
```typescript
// Good: Use ApiMockFactory for consistent responses
const { mockFetch, mockResponse } = MockSetup.setupApiMocks()
mockFetch.mockResolvedValue(mockResponse({ id: '123', name: 'Test' }))
```

## Test Categories

### Unit Tests
- **Purpose**: Test individual functions or classes in isolation
- **Location**: Same directory as source code
- **Environment**: `node`
- **Setup**: `tests/setup/global-mocks.ts`

### Integration Tests
- **Purpose**: Test interaction between multiple components/services
- **Location**: `tests/integration/`
- **Environment**: `node` with database mocks
- **Setup**: `tests/setup/global-mocks.ts`, `tests/setup/database.ts`

### Component Tests
- **Purpose**: Test React components with user interactions
- **Location**: `tests/components/` or alongside components
- **Environment**: `jsdom`
- **Setup**: `tests/setup/global-mocks.ts`, `tests/setup/component.ts`

### E2E Tests
- **Purpose**: Test complete user workflows
- **Location**: `e2e/`
- **Environment**: `jsdom` with Playwright
- **Setup**: `tests/setup/global-mocks.ts`, `tests/setup/e2e.ts`

## Business Logic Testing

### Authorization Tests
```typescript
describe('authorization', () => {
  it('should allow admin to access admin functions', async () => {
    const adminSession = MockFactory.session(MockFactory.admin())
    TestEnvironment.setupComplete(adminSession)
    
    await expect(adminFunction()).resolves.not.toThrow()
  })

  it('should deny regular user access to admin functions', async () => {
    const userSession = MockFactory.session(MockFactory.user())
    TestEnvironment.setupComplete(userSession)
    
    await expect(adminFunction()).rejects.toThrow('Unauthorized')
  })
})
```

### Data Validation Tests
```typescript
describe('validation', () => {
  it('should reject invalid product data', async () => {
    const invalidProduct = MockFactory.product({ 
      price: -10, // Invalid negative price
      name: '',    // Empty name
    })
    
    AssertionHelpers.assertValidationPrevents(
      invalidProduct,
      createProduct,
      /Invalid product data/
    )
  })

  it('should accept valid product data', async () => {
    const validProduct = MockFactory.product()
    
    await expect(createProduct(validProduct)).resolves.toHaveProperty('id')
  })
})
```

### Transaction Tests
```typescript
describe('transactions', () => {
  it('should rollback on error', async () => {
    const { mockClient } = MockSetup.setupDatabaseMocks()
    mockClient.query
      .mockResolvedValueOnce({}) // BEGIN
      .mockRejectedValueOnce(new Error('Database error')) // Operation fails
    
    await expect(failingOperation()).rejects.toThrow()
    
    expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK')
    expect(mockClient.release).toHaveBeenCalled()
  })
})
```

## Component Testing Patterns

### Behavior Verification
```typescript
describe('AddToCartButton', () => {
  it('should add product to cart when clicked', async () => {
    const product = MockFactory.product()
    const user = MockSetup.mockUserEvent()
    
    renderWithProviders(<AddToCartButton product={product} />)
    
    await user.click(screen.getByRole('button', { name: /add to cart/i }))
    
    // Verify behavior (not implementation)
    expect(screen.getByText(/added to cart/i)).toBeInTheDocument()
    // Verify that cart state changed (through context/store)
    expect(mockCartContext.addItem).toHaveBeenCalledWith(product)
  })
})
```

### Error State Testing
```typescript
it('should show error when product is out of stock', () => {
  const outOfStockProduct = MockFactory.createOutOfStockProduct()
  
  renderWithProviders(<AddToCartButton product={outOfStockProduct} />)
  
  expect(screen.getByText(/out of stock/i)).toBeInTheDocument()
  expect(screen.getByRole('button')).toBeDisabled()
})
```

## Performance and Load Testing

### Database Query Optimization
```typescript
describe('performance', () => {
  it('should use efficient pagination', async () => {
    const { mockClient } = MockSetup.setupDatabaseMocks()
    mockClient.query.mockResolvedValue(DatabaseMockFactory.emptyQuery())
    
    await getProducts(100, 1000) // Large offset
    
    // Verify LIMIT and OFFSET are used correctly
    expect(mockClient.query).toHaveBeenCalledWith(
      expect.stringMatching(/SELECT.*LIMIT \$1.*OFFSET \$2/),
      [100, 1000]
    )
  })
})
```

## Security Testing

### Input Validation
```typescript
describe('security', () => {
  it('should sanitize user input', async () => {
    const maliciousInput = '<script>alert("xss")</script>'
    
    const result = await processUserInput(maliciousInput)
    
    expect(result).not.toContain('<script>')
    expect(result).toContain('&lt;script&gt;')
  })

  it('should validate UUID format', async () => {
    const invalidId = 'not-a-uuid'
    
    await expect(getProductById(invalidId)).rejects.toThrow('Invalid ID format')
  })
})
```

## Test Data Management

### Use Factory Pattern
```typescript
// Good: Use factories for consistent test data
const products = TestDataFactory.createMany(
  () => TestDataFactory.createProduct(),
  10
)

// Good: Create specific variants
const expensiveProduct = TestDataFactory.createProduct({
  price: 999.99,
  name: 'Luxury Item',
})

const outOfStockProduct = TestDataFactory.createOutOfStockProduct()
```

### Avoid Hardcoded Test Data
```typescript
// ❌ Bad: Hardcoded IDs
it('should find product with ID 123', () => {
  const product = await findProduct('123')
  expect(product).toBeDefined()
})

// ✅ Good: Use factory-generated IDs
it('should find product with valid ID', () => {
  const product = TestDataFactory.createProduct()
  const result = await findProduct(product.id)
  expect(result.id).toBe(product.id)
})
```

## Error Handling Testing

### Test Both Success and Failure Paths
```typescript
describe('error handling', () => {
  it('should handle network errors gracefully', async () => {
    const { mockFetch } = MockSetup.setupApiMocks()
    mockFetch.mockRejectedValue(new Error('Network error'))
    
    const result = await fetchProducts()
    
    AssertionHelpers.assertApiResponseStructure(result, 'error')
    expect(result.error).toContain('Unable to fetch products')
  })

  it('should log errors appropriately', async () => {
    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
    
    // Trigger error
    await failingOperation()
    
    expect(consoleSpy).toHaveBeenCalledWith(
      'Operation failed:',
      expect.any(Error)
    )
    
    consoleSpy.mockRestore()
  })
})
```

## Test Naming Conventions

### Describe Block Organization
```typescript
describe('UserService', () => {
  describe('when user is authenticated', () => {
    describe('with admin role', () => {
      it('should create new users')
      it('should delete existing users')
      it('should update user permissions')
    })
    
    describe('with regular user role', () => {
      it('should view own profile')
      it('should update own profile')
      it('should not access other users')
    })
  })
  
  describe('when user is not authenticated', () => {
    it('should redirect to login')
    it('should return 401 for protected routes')
  })
})
```

### Test Name Patterns
```typescript
// Good: Clear, behavior-focused names
it('should add product to cart when user clicks button')
it('should reject negative prices with validation error')
it('should rollback transaction when database operation fails')
it('should show loading state while fetching data')

// Bad: Implementation-focused names
it('should call handleAddToCart')
it('should set isValid to false')
it('should execute ROLLBACK query')
it('should set isLoading to true')
```

## Coverage Requirements

### Minimum Coverage Thresholds
- **Global**: 100% lines, functions, branches, statements
- **Critical modules** (`lib/`, `actions/`, `schemas/`): 100% coverage
- **Components**: 95% coverage (exceptions for UI-only components)

### Focus on Meaningful Coverage
- Test business logic paths, not getter/setter methods
- Ensure edge cases are covered
- Verify error handling paths
- Test security boundaries

## Running Tests

### Command Reference
```bash
# Run all tests
npm run test

# Run specific test type
npm run test:unit      # Unit tests only
npm run test:component # Component tests only
npm run test:integration # Integration tests only

# Watch mode for development
npm run test:watch

# Coverage report
npm run test:coverage

# Comprehensive test suite
npm run test:comprehensive
```

## Common Pitfalls to Avoid

### 1. Over-mocking
❌ Don't mock everything - mock only external dependencies
✅ Use real implementations where possible

### 2. Testing Implementation Details
❌ Don't test private methods or internal state
✅ Test public interface and observable behavior

### 3. Brittle Tests
❌ Don't depend on exact implementation order
✅ Focus on final state and side effects

### 4. Inconsistent Test Data
❌ Don't use hardcoded values throughout tests
✅ Use factories and consistent data patterns

### 5. Missing Edge Cases
❌ Don't only test happy path
✅ Test error conditions, boundary values, and invalid input

## Review Checklist

Before submitting code with tests, ensure:

- [ ] Tests follow the structure and naming conventions
- [ ] Mock setup uses the provided utilities
- [ ] Business logic is tested, not implementation
- [ ] Both success and error paths are covered
- [ ] Authorization is properly tested
- [ ] Input validation is verified
- [ ] Test data uses factory patterns
- [ ] Coverage thresholds are met
- [ ] No over-mocking or under-mocking
- [ ] Tests are isolated and don't depend on each other

By following these guidelines, we ensure our tests are reliable, maintainable, and provide meaningful confidence in our codebase.