Comprehensive testing for business logic, authorization validation, and service layer integration patterns.
The service layer contains the core business logic of your application, making it critical to test thoroughly. Service tests focus on:
Service Layer Focus: Test business rules, not implementation details. Services should work correctly regardless of how the underlying database or external APIs function.
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:
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
})
}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)
})
})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')
})
})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 specificallyService 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.
1. Isolation & Mocking
2. Authorization First
3. Error Handling
4. Business Logic Focus
5. Realistic Test Data
Ready to implement end-to-end testing? Continue with E2E Testing to test complete user workflows with Playwright.