Bluewoo HRMS

Testing Strategy

Testing approach, coverage targets, and tools

Testing Strategy

Coverage Targets

ComponentMinimum Coverage
Services90%
Repositories80%
Controllers70%
Components70%
Overall80%

Test Distribution

TypeProportionPurpose
Unit Tests70%Individual functions, services
Integration Tests20%API endpoints, DB operations
E2E Tests10%Critical user flows

Testing Tools

Backend (NestJS)

  • Jest - Test runner
  • Supertest - HTTP testing
  • Prisma mock - Database mocking

Frontend (Next.js)

  • Jest - Test runner
  • React Testing Library - Component testing
  • Playwright - E2E browser testing

What to Test

Must Test

  • Business logic in services
  • API endpoint responses
  • Authentication flows
  • Permission checks (RBAC)
  • Tenant isolation (critical!)
  • Form validation

Can Skip

  • Simple CRUD pass-through
  • Framework internals
  • Third-party library code

Test Structure

backend/test/
├── unit/           # Service, repository tests
├── integration/    # API endpoint tests
└── e2e/            # Full flow tests

frontend/__tests__/
├── unit/           # Component, hook tests
├── integration/    # Page tests with mocked API
└── e2e/            # Playwright browser tests

Critical Tests

Tenant Isolation

Every feature must verify:

  • User A in Tenant 1 cannot see Tenant 2 data
  • Queries always include tenantId filter
  • Cross-tenant access returns 403

Authentication

  • Login/logout flows work
  • JWT tokens validate correctly
  • Expired tokens rejected
  • Invalid credentials handled

Permissions

  • Platform admin can access all tenants
  • Tenant admin limited to own tenant
  • User permissions enforced on endpoints

Best Practices

  1. Test behavior, not implementation
  2. Use factories for test data
  3. Clean up after each test
  4. Mock external services
  5. Use data-testid for E2E selectors
  6. Run tests in CI on every PR

Test Data Factories

Use factories to create consistent test data:

// test/factories/employee.factory.ts
import { faker } from '@faker-js/faker'
import { Employee, EmployeeStatus } from '@prisma/client'

export function createTestEmployee(overrides: Partial<Employee> = {}): Omit<Employee, 'id' | 'createdAt' | 'updatedAt'> {
  return {
    tenantId: overrides.tenantId ?? faker.string.uuid(),
    firstName: faker.person.firstName(),
    lastName: faker.person.lastName(),
    email: faker.internet.email(),
    employeeNumber: faker.string.alphanumeric(6).toUpperCase(),
    status: EmployeeStatus.ACTIVE,
    hireDate: faker.date.past(),
    departmentId: null,
    managerId: null,
    deletedAt: null,
    ...overrides,
  }
}

export function createTestTenant(overrides = {}) {
  return {
    name: faker.company.name(),
    domain: faker.internet.domainName(),
    status: 'ACTIVE',
    ...overrides,
  }
}

Prisma Mocking for Unit Tests

// test/mocks/prisma.mock.ts
import { PrismaClient } from '@prisma/client'
import { mockDeep, DeepMockProxy } from 'jest-mock-extended'

export type MockPrismaClient = DeepMockProxy<PrismaClient>

export const createMockPrisma = (): MockPrismaClient => mockDeep<PrismaClient>()

// Usage in test file
describe('EmployeeService', () => {
  let service: EmployeeService
  let prisma: MockPrismaClient

  beforeEach(() => {
    prisma = createMockPrisma()
    service = new EmployeeService(prisma)
  })

  it('should find employee by id', async () => {
    const mockEmployee = createTestEmployee({ id: 'emp-123' })
    prisma.employee.findFirst.mockResolvedValue(mockEmployee as any)

    const result = await service.findById('emp-123', 'tenant-1')

    expect(result).toEqual(mockEmployee)
    expect(prisma.employee.findFirst).toHaveBeenCalledWith({
      where: { id: 'emp-123', tenantId: 'tenant-1' },
    })
  })
})

Integration Test Setup

// test/integration/setup.ts
import { PrismaClient } from '@prisma/client'
import { createTestTenant, createTestEmployee } from '../factories'

const prisma = new PrismaClient()

export async function setupTestDatabase() {
  // Clean up before tests
  await prisma.employee.deleteMany()
  await prisma.tenant.deleteMany()

  // Seed base data
  const tenant = await prisma.tenant.create({
    data: createTestTenant({ id: 'test-tenant-1' }),
  })

  const admin = await prisma.employee.create({
    data: createTestEmployee({
      tenantId: tenant.id,
      email: 'admin@test.com',
    }),
  })

  return { tenant, admin }
}

export async function teardownTestDatabase() {
  await prisma.$disconnect()
}

Playwright E2E Test Structure

// e2e/employees.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Employee Management', () => {
  test.beforeEach(async ({ page }) => {
    // Login as admin
    await page.goto('/login')
    await page.fill('[data-testid="email-input"]', 'admin@test.com')
    await page.fill('[data-testid="password-input"]', 'password')
    await page.click('[data-testid="login-button"]')
    await page.waitForURL('/dashboard')
  })

  test('should display employee list', async ({ page }) => {
    await page.goto('/employees')

    await expect(page.getByTestId('employee-list')).toBeVisible()
    await expect(page.getByTestId('employee-row')).toHaveCount(10)
  })

  test('should create new employee', async ({ page }) => {
    await page.goto('/employees/new')

    await page.fill('[data-testid="first-name"]', 'John')
    await page.fill('[data-testid="last-name"]', 'Doe')
    await page.fill('[data-testid="email"]', 'john.doe@company.com')
    await page.click('[data-testid="submit-button"]')

    await expect(page.getByText('Employee created successfully')).toBeVisible()
  })
})

Running Tests

# Unit tests
npm test

# Integration tests
npm test:integration

# E2E tests
npm test:e2e

# Coverage report
npm test:coverage

# Watch mode
npm test:watch

Test patterns to be refined during development