General Testing

Foundation testing patterns, utilities, and setup configurations used throughout the SaaS boilerplate testing suite.

Table of Contents

Test Setup and Configuration

Essential setup files and configuration patterns:
Client-Side Test Setup:
// src/__tests__/setup-test.ts
import { beforeAll, afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
import '@testing-library/jest-dom'

// Configure jsdom environment
beforeAll(() => {
  // Mock window objects
  Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: vi.fn().mockImplementation(query => ({
      matches: false,
      media: query,
      onchange: null,
      addListener: vi.fn(),
      removeListener: vi.fn(),
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
    })),
  })

  // Mock localStorage
  Object.defineProperty(window, 'localStorage', {
    value: {
      getItem: vi.fn(),
      setItem: vi.fn(),
      removeItem: vi.fn(),
      clear: vi.fn(),
    },
  })

  // Mock ResizeObserver
  global.ResizeObserver = vi.fn().mockImplementation(() => ({
    observe: vi.fn(),
    unobserve: vi.fn(),
    disconnect: vi.fn(),
  }))
})

// Cleanup after each test
afterEach(() => {
  cleanup()
  vi.clearAllMocks()
})
Key Features:
  • jsdom Environment: DOM testing for React components
  • Mock Browser APIs: localStorage, matchMedia, ResizeObserver
  • Automatic Cleanup: Prevents test pollution
  • Jest DOM Matchers: Enhanced assertion capabilities

Mocking Patterns

Comprehensive mocking strategies for different types of dependencies:
Authentication Service Mocking:
// src/services/__tests__/helper-service-test.ts
import { vi } from 'vitest'
import { getAuthUser } from '@/services/authentication/auth-service'
import { User } from '@/services/types/domain/user-types'

export const setupAuthUserMocked = async (user?: User | undefined) => {
  return vi.mocked(getAuthUser).mockImplementation(async () => {
    if (!user) return undefined
    return user
  })
}

// Usage patterns in tests
describe('Service with Authentication', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('should allow access for authenticated user', async () => {
    const testUser = createTestUser({ role: 'admin' })
    setupAuthUserMocked(testUser)

    const result = await protectedService()
    expect(result).toBeDefined()
  })

  it('should deny access for unauthenticated user', async () => {
    setupAuthUserMocked() // No user (undefined)

    await expect(protectedService()).rejects.toThrow('Unauthorized')
  })
})
Better Auth Session Mocking:
// Mock Better Auth context
vi.mock('@/lib/auth/auth-client', () => ({
  authClient: {
    getSession: vi.fn(),
    signIn: vi.fn(),
    signOut: vi.fn(),
  },
}))

// Mock session data
const mockSession = {
  user: {
    id: 'user-123',
    email: 'test@example.com',
    name: 'Test User',
  },
  session: {
    id: 'session-123',
    expiresAt: new Date(Date.now() + 86400000), // 24 hours
  },
}

vi.mocked(authClient.getSession).mockResolvedValue(mockSession)

Test Data Factories

Reusable test data generation patterns:
User Data Factory:
// src/__tests__/factories/user-factory.ts
import { User } from '@/services/types/domain/user-types'
import { RoleConst } from '@/services/types/domain/auth-types'

export const createTestUser = (overrides: Partial<User> = {}): User => {
  return {
    id: crypto.randomUUID(),
    name: 'Test User',
    email: 'test@example.com',
    emailVerified: true,
    image: null,
    role: RoleConst.USER,
    visibility: 'private',
    banned: null,
    banReason: null,
    banExpires: null,
    createdAt: new Date(),
    updatedAt: new Date(),
    stripeCustomerId: null,
    twoFactorEnabled: false,
    ...overrides,
  }
}

// Specialized user factories
export const createAdminUser = (overrides: Partial<User> = {}): User => {
  return createTestUser({
    role: RoleConst.ADMIN,
    name: 'Admin User',
    email: 'admin@example.com',
    ...overrides,
  })
}

export const createPublicUser = (overrides: Partial<User> = {}): User => {
  return createTestUser({
    visibility: 'public',
    name: 'Public User',
    email: 'public@example.com',
    ...overrides,
  })
}

// Usage in tests
describe('User Service', () => {
  it('should handle admin users', () => {
    const adminUser = createAdminUser()
    expect(adminUser.role).toBe(RoleConst.ADMIN)
  })
})

Common Utilities

Shared testing utilities and helper functions:
Custom Assertion Utilities:
// src/__tests__/utils/assertions.ts
import { expect } from 'vitest'

// UUID validation
export const expectValidUUID = (value: string) => {
  expect(value).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
}

// Date assertions
export const expectRecentDate = (date: Date | string, withinMs = 5000) => {
  const dateObj = typeof date === 'string' ? new Date(date) : date
  const now = new Date()
  const diff = Math.abs(now.getTime() - dateObj.getTime())
  expect(diff).toBeLessThan(withinMs)
}

// Email format validation
export const expectValidEmail = (email: string) => {
  expect(email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
}

// Authorization error assertion
export const expectAuthorizationError = async (promise: Promise<any>) => {
  await expect(promise).rejects.toThrowError('Accès non autorisé')
}

// Object structure validation
export const expectUserStructure = (user: any) => {
  expect(user).toEqual(expect.objectContaining({
    id: expect.stringMatching(/^[0-9a-f-]{36}$/),
    email: expect.stringMatching(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
    name: expect.any(String),
    role: expect.stringMatching(/^(guest|user|moderator|admin|super_admin)$/),
    createdAt: expect.any(Date),
  }))
}

// Usage in tests
describe('User Service', () => {
  it('should create user with valid structure', async () => {
    const user = await createUserService(userData)

    expectUserStructure(user)
    expectValidUUID(user.id)
    expectValidEmail(user.email)
    expectRecentDate(user.createdAt)
  })
})

Best Practices

General Testing Principles:
  • Consistency: Use factories and utilities for consistent test data
  • Isolation: Each test should be independent and repeatable
  • Clarity: Test names and structure should be self-documenting
  • Performance: Mock external dependencies to keep tests fast

Key Recommendations:

  1. Use Factories: Create reusable data factories for consistent test data
  2. Mock External Services: Keep tests focused on your code, not third-party services
  3. Clean Up: Always clean up after tests to prevent pollution
  4. Type Safety: Leverage TypeScript for better test reliability
  5. Shared Utilities: Create helper functions for common testing patterns
Ready to dive deeper into specific testing layers? Continue with Unit Testing to learn about testing pure functions and business logic.
    General Testing | ShipSaaS Documentation | ShipSaaS - Launch your SaaS with AI in days