Service Testing

Comprehensive testing for business logic, authorization validation, and service layer integration patterns.

Overview

The service layer contains the core business logic of your application, making it critical to test thoroughly. Service tests focus on:
  • Business Logic Validation - Ensure operations work correctly
  • Authorization Testing - Verify RBAC permissions are enforced
  • Integration Testing - Test service dependencies and interactions
  • Error Handling - Validate proper error responses
Service Layer Focus: Test business rules, not implementation details. Services should work correctly regardless of how the underlying database or external APIs function.

Testing Architecture

Service tests follow a 3-layer mocking strategy that isolates business logic:
Layer Isolation Pattern:
// 1. Mock Repository Layer (Data Access)
vi.mock('@/db/repositories/user-repository', () => ({
  getUserByIdDao: vi.fn(),
  createUserSettingsDao: vi.fn(),
  updateUserSettingsDao: vi.fn(),
}))

// 2. Mock External Services
vi.mock('@/services/facades/subscription-service-facade', () => ({
  getActivePlansForBetterAuthService: vi.fn(),
}))

// 3. Mock Authentication
import { setupAuthUserMocked } from './helper-service-test'
Benefits:
  • Fast Execution - No database or network calls
  • Predictable Results - Controlled test data
  • Focused Testing - Only tests business logic
  • Independent Tests - No external dependencies

Core Testing Patterns

Essential patterns used across all service tests:
Test Setup Pattern:
// src/services/__tests__/user-service.test.ts
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { setupAuthUserMocked } from './helper-service-test'
import * as userRepository from '@/db/repositories/user-repository'
import { getUserByIdService } from '../user-service'

const userTest = {
  id: 'ae760f8e-4aa6-4d71-a4c8-344429b7ae21',
  name: 'Test User',
  email: 'test@example.com',
  role: RoleConst.USER,
  emailVerified: true,
  visibility: 'private',
  createdAt: new Date(),
  updatedAt: new Date(),
} satisfies User

describe('User Service Tests', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.mocked(userRepository.getUserByIdDao).mockResolvedValue(userTest)
  })
})
Mock Helper Utility:
// src/services/__tests__/helper-service-test.ts
export const setupAuthUserMocked = async (user?: User | undefined) => {
  return vi.mocked(getAuthUser).mockImplementation(async () => {
    if (!user) return
    return user
  })
}

CASL Authorization Testing

Testing role-based access control with CASL abilities:
Basic Permission Tests:
// src/services/authorization/__tests__/casl-authorization-service.test.ts
import { userCan, userCannot, defineAbilitiesFor } from '../authorization-service'
import { ActionsConst, SubjectsConst } from '../casl-abilities'

describe('CASL Abilities', () => {
  const regularUser = {
    id: 'user-123',
    role: RoleConst.USER,
    // ... other properties
  }

  const adminUser = {
    id: 'admin-123',
    role: RoleConst.ADMIN,
    // ... other properties
  }

  describe('User Permissions', () => {
    it('regular user can read subscriptions', () => {
      expect(
        userCan(regularUser, ActionsConst.READ, SubjectsConst.SUBSCRIPTION)
      ).toBe(true)
    })

    it('regular user cannot delete users', () => {
      expect(
        userCan(regularUser, ActionsConst.DELETE, SubjectsConst.USER)
      ).toBe(false)

      expect(
        userCannot(regularUser, ActionsConst.DELETE, SubjectsConst.USER)
      ).toBe(true)
    })
  })

  describe('Admin Permissions', () => {
    it('admin can manage all users', () => {
      expect(
        userCan(adminUser, ActionsConst.MANAGE, SubjectsConst.USER)
      ).toBe(true)
    })

    it('admin can perform all actions on subscriptions', () => {
      expect(userCan(adminUser, ActionsConst.CREATE, SubjectsConst.SUBSCRIPTION)).toBe(true)
      expect(userCan(adminUser, ActionsConst.READ, SubjectsConst.SUBSCRIPTION)).toBe(true)
      expect(userCan(adminUser, ActionsConst.UPDATE, SubjectsConst.SUBSCRIPTION)).toBe(true)
      expect(userCan(adminUser, ActionsConst.DELETE, SubjectsConst.SUBSCRIPTION)).toBe(true)
    })
  })
})
Guest User Testing:
describe('Guest (Unauthenticated) Permissions', () => {
  const guestUser = undefined

  it('can read public user profiles', () => {
    const publicProfile = { id: 'user-123', visibility: 'public' }
    expect(
      userCanOnResource(guestUser, ActionsConst.READ, SubjectsConst.USER, publicProfile)
    ).toBe(true)
  })

  it('cannot read private user profiles', () => {
    const privateProfile = { id: 'user-123', visibility: 'private' }
    expect(
      userCanOnResource(guestUser, ActionsConst.READ, SubjectsConst.USER, privateProfile)
    ).toBe(false)
  })

  it('cannot create users', () => {
    expect(userCan(guestUser, ActionsConst.CREATE, SubjectsConst.USER)).toBe(false)
  })
})

Integration Testing Patterns

Testing service interactions and external dependencies:
Service-to-Service Testing:
describe('Service Integration', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('should integrate user and subscription services', async () => {
    // Mock subscription service facade
    vi.mocked(getActivePlansForBetterAuthService).mockResolvedValue([
      { id: 'plan-1', name: 'Basic Plan', price: 999 }
    ])

    // Test user service calling subscription service
    setupAuthUserMocked(userTest)
    const userWithSubscriptions = await getUserWithSubscriptionsService(userTest.id)

    expect(userWithSubscriptions.subscriptions).toHaveLength(1)
    expect(getActivePlansForBetterAuthService).toHaveBeenCalledWith(userTest.id)
  })

  it('should handle service dependency failures', async () => {
    // Mock service failure
    vi.mocked(getActivePlansForBetterAuthService).mockRejectedValue(
      new Error('Subscription service unavailable')
    )

    setupAuthUserMocked(userTest)

    // Should handle gracefully or propagate appropriately
    await expect(
      getUserWithSubscriptionsService(userTest.id)
    ).rejects.toThrow('Subscription service unavailable')
  })
})
Facade Integration Testing:
// Test service facades work correctly
describe('Service Facades', () => {
  it('should expose simplified interface to app layer', async () => {
    setupAuthUserMocked(userTest)
    vi.mocked(userRepository.getUserByIdDao).mockResolvedValue(userTest)

    // Import facade function
    const { getUserById } = await import('@/services/facades/user-service-facade')

    const result = await getUserById(userTest.id)
    expect(result).toEqual(userTest)
  })

  it('should handle facade errors consistently', async () => {
    setupAuthUserMocked(userTest)
    vi.mocked(userRepository.getUserByIdDao).mockRejectedValue(
      new Error('Database error')
    )

    const { getUserById } = await import('@/services/facades/user-service-facade')

    await expect(getUserById(userTest.id)).rejects.toThrow('Database error')
  })
})

Running Service Tests

Essential commands for service testing workflow:
Daily Testing Workflow:
# Run all service tests
pnpm test services/
pnpm test src/services/__tests__/

# Run specific service tests
pnpm test user-service
pnpm test authorization
pnpm test src/services/__tests__/user-service.test.ts

# Watch mode for active development
pnpm test:watch services/
pnpm test:watch src/services/__tests__/user-service.test.ts

# Run with coverage
pnpm test:coverage services/
Interactive Development:
# Open Vitest UI for service tests
pnpm test:ui --root src/services

# Run tests matching pattern
pnpm test --grep="Authorization"
pnpm test --grep="CRUD Operations"
pnpm test --grep="\\[ADMIN\\]" # Test admin role specifically

Best Practices

Service testing guidelines for maintainable, reliable tests:
Service Testing Philosophy: Test business logic behavior, not implementation details. Services should work correctly regardless of underlying technology changes.

Key Principles

1. Isolation & Mocking
  • Mock all external dependencies (repositories, APIs, services)
  • Use consistent mocking patterns across tests
  • Clear mocks between tests to prevent pollution
2. Authorization First
  • Test authorization before business logic
  • Cover all role combinations and permission scenarios
  • Use realistic test data that matches production constraints
3. Error Handling
  • Test both success and failure scenarios
  • Verify error types and messages are appropriate
  • Ensure graceful degradation when external services fail
4. Business Logic Focus
  • Test domain rules and constraints
  • Validate input/output transformations
  • Cover edge cases and boundary conditions
5. Realistic Test Data
  • Use test data factories for consistency
  • Match production data patterns and constraints
  • Include edge cases in test data scenarios
Ready to implement end-to-end testing? Continue with E2E Testing to test complete user workflows with Playwright.
    Service Testing | ShipSaaS Documentation | ShipSaaS - Launch your SaaS with AI in days