Setup & Teardown

Overview

Setup and teardown hooks are essential for managing test environment state, initializing resources, cleaning up after tests, and ensuring test isolation. Fauji provides comprehensive lifecycle hooks that work seamlessly with both synchronous and asynchronous operations.

Fauji's Setup and Teardown Features

Fauji provides the same familiar lifecycle hooks you're used to from Jest, with full support for:

  • Async Operations - All hooks support async/await for database setup, API calls, file operations
  • Error Handling - Graceful handling of setup failures with proper cleanup
  • Nested Scopes - Hooks work seamlessly with nested describe blocks
  • Resource Cleanup - Automatic cleanup of mocks, timers, and other resources
  • Integration - Works perfectly with Fauji's mocking system and fake timers

Lifecycle Hooks

Fauji provides four main lifecycle hooks that execute at different points in the test lifecycle:

beforeAll(fn)

Runs once before all tests in the current describe block. Perfect for expensive setup operations that can be shared across tests.

afterAll(fn)

Runs once after all tests in the current describe block have completed. Used for cleanup operations like closing connections or removing test data.

beforeEach(fn)

Runs before each individual test. Ensures every test starts with a fresh, predictable state.

afterEach(fn)

Runs after each individual test completes. Used for per-test cleanup and state reset.

Basic Usage Patterns

Simple Setup and Teardown

            
describe('Calculator', () => {
  let calculator;

  beforeAll(() => {
    // One-time setup for the entire test suite
    console.log('Starting Calculator tests');
  });

  beforeEach(() => {
    // Fresh calculator instance for each test
    calculator = new Calculator();
  });

  afterEach(() => {
    // Clean up after each test
    calculator.reset();
    calculator = null;
  });

  afterAll(() => {
    // One-time cleanup after all tests
    console.log('Calculator tests completed');
  });

  test('length is 1 after push', () => {
    expect(arr).toHaveLength(1);
  });
});
  

Nested Describe Blocks

            
describe('User Management', () => {
  let userService;

  beforeAll(() => {
    // Setup for entire User Management suite
    userService = new UserService();
  });

  afterAll(() => {
    // Cleanup for entire User Management suite
    userService.disconnect();
  });

  describe('User Creation', () => {
    let testUser;

    beforeEach(() => {
      // Setup specific to User Creation tests
      testUser = {
        name: 'Test User',
        email: 'test@example.com'
      };
    });

    afterEach(() => {
      // Cleanup specific to User Creation tests
      if (testUser.id) {
        userService.deleteUser(testUser.id);
      }
    });

    test('creates user with valid data', () => {
      const result = userService.createUser(testUser);
      expect(result).toMatchObject({
        id: expect.toBeString(),
        name: 'Test User'
      });
    });

    test('validates required fields', () => {
      expect(() => userService.createUser({})).toThrow('Name is required');
    });
  });

  describe('User Validation', () => {
    beforeEach(() => {
      // Different setup for validation tests
      userService.setValidationMode('strict');
    });

    afterEach(() => {
      // Reset validation mode
      userService.setValidationMode('normal');
    });

    test('enforces email format', () => {
      expect(() => userService.createUser({
        name: 'John',
        email: 'invalid-email'
      })).toThrow('Invalid email format');
    });
  });
});
  

Async Setup and Teardown

All lifecycle hooks support async operations with promises and async/await.

Database Setup Example

            
describe('Database Operations', () => {
  let db;
  let connection;

  beforeAll(async () => {
    // Connect to test database
    connection = await createTestConnection();
    db = connection.getDatabase('test_db');
    
    // Run migrations
    await db.migrate();
    console.log('Database setup completed');
  });

  beforeEach(async () => {
    // Start a transaction for each test
    await db.beginTransaction();
    
    // Seed with test data
    await db.users.create({
      name: 'Default User',
      email: 'default@example.com'
    });
  });

  afterEach(async () => {
    // Rollback transaction to clean state
    await db.rollback();
  });

  afterAll(async () => {
    // Close database connection
    await connection.close();
    console.log('Database cleanup completed');
  });

  test('creates user successfully', async () => {
    const user = await db.users.create({
      name: 'John Doe',
      email: 'john@example.com'
    });

    expect(user).toMatchObject({
      id: expect.toBeString(),
      name: 'John Doe'
    });
  });

  test('finds users by email', async () => {
    const user = await db.users.findByEmail('default@example.com');
    expect(user).toMatchObject({ name: 'Default User' });
  });
});
  

API Testing Setup

            
describe('API Integration Tests', () => {
  let server;
  let apiClient;

  beforeAll(async () => {
    // Start test server
    server = await startTestServer({
      port: 3001,
      environment: 'test'
    });
    
    // Initialize API client
    apiClient = new APIClient({
      baseURL: 'http://localhost:3001',
      timeout: 5000
    });

    // Wait for server to be ready
    await waitForServer(server);
  });

  beforeEach(async () => {
    // Reset database state
    await server.resetDatabase();
    
    // Create test authentication token
    const authResponse = await apiClient.post('/auth/test-token', {
      userId: 'test-user-123'
    });
    
    apiClient.setAuthToken(authResponse.data.token);
  });

  afterEach(async () => {
    // Clear authentication
    apiClient.clearAuthToken();
    
    // Clear any uploaded files
    await server.clearTempFiles();
  });

  afterAll(async () => {
    // Shutdown test server
    await server.stop();
  });

  test('GET /users returns user list', async () => {
    const response = await apiClient.get('/users');
    
    expect(response.status).toBe(200);
    expect(response.data).toBeArray();
  });

  test('POST /users creates new user', async () => {
    const userData = {
      name: 'Jane Doe',
      email: 'jane@example.com'
    };

    const response = await apiClient.post('/users', userData);
    
    expect(response.status).toBe(201);
    expect(response.data).toMatchObject(userData);
  });
});
  

File System and Resource Management

Temporary File Management

            
describe('File Processing', () => {
  let tempDir;
  let testFiles;

  beforeAll(async () => {
    // Create temporary directory
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fauji-test-'));
  });

  beforeEach(async () => {
    // Create test files for each test
    testFiles = {
      input: path.join(tempDir, 'input.txt'),
      output: path.join(tempDir, 'output.txt'),
      config: path.join(tempDir, 'config.json')
    };

    await fs.writeFile(testFiles.input, 'test input data');
    await fs.writeFile(testFiles.config, JSON.stringify({
      format: 'text',
      encoding: 'utf8'
    }));
  });

  afterEach(async () => {
    // Clean up test files
    for (const filePath of Object.values(testFiles)) {
      try {
        await fs.unlink(filePath);
      } catch (error) {
        // File might not exist, ignore error
      }
    }
  });

  afterAll(async () => {
    // Remove temporary directory
    await fs.rmdir(tempDir, { recursive: true });
  });

  test('processes file correctly', async () => {
    await processFile(testFiles.input, testFiles.output, testFiles.config);
    
    const result = await fs.readFile(testFiles.output, 'utf8');
    expect(result).toContain('processed: test input data');
  });

  test('handles missing input file', async () => {
    await fs.unlink(testFiles.input);
    
    await expect(
      processFile(testFiles.input, testFiles.output, testFiles.config)
    ).rejects.toThrow(/ENOENT/);
  });
});
  

Mock and Timer Management

            
describe('Time-dependent Operations', () => {
  let originalFetch;
  let mockFetch;

  beforeAll(() => {
    // Setup global mocks
    originalFetch = global.fetch;
    mockFetch = fn();
    global.fetch = mockFetch;
  });

  beforeEach(() => {
    // Reset mocks and enable fake timers
    mockFetch.mockClear();
    useFakeTimers();
    
    // Default mock implementation
    mockFetch.mockResolvedValue({
      ok: true,
      json: async () => ({ data: 'test data' })
    });
  });

  afterEach(() => {
    // Restore real timers
    useRealTimers();
    
    // Reset all mocks
    resetAllMocks();
  });

  afterAll(() => {
    // Restore original globals
    global.fetch = originalFetch;
  });

  test('retries failed requests with backoff', async () => {
    // Setup fetch to fail first two times
    mockFetch
      .mockRejectedValueOnce(new Error('Network error'))
      .mockRejectedValueOnce(new Error('Network error'))
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true })
      });

    const resultPromise = retryFetch('/api/data', { 
      maxRetries: 3,
      baseDelay: 1000 
    });

    // Fast-forward through retry delays
    advanceTimersByTime(1000); // First retry
    advanceTimersByTime(2000); // Second retry (exponential backoff)

    const result = await resultPromise;
    
    expect(result).toMatchObject({ success: true });
    expect(mockFetch).toHaveBeenCalledTimes(3);
  });

  test('caches responses with TTL', async () => {
    const cache = new ResponseCache({ ttl: 5000 });
    
    // First call should hit the network
    await cache.get('/api/user/1');
    expect(mockFetch).toHaveBeenCalledTimes(1);
    
    // Second call should use cache
    await cache.get('/api/user/1');
    expect(mockFetch).toHaveBeenCalledTimes(1);
    
    // Advance past TTL
    advanceTimersByTime(6000);
    
    // Third call should hit network again
    await cache.get('/api/user/1');
    expect(mockFetch).toHaveBeenCalledTimes(2);
  });
});
  

Error Handling in Hooks

Proper error handling in setup and teardown hooks is crucial for maintaining test reliability.

Graceful Error Handling

            
describe('Robust Test Setup', () => {
  let server;
  let database;

  beforeAll(async () => {
    try {
      // Start database first
      database = await startTestDatabase();
      
      // Then start server
      server = await startTestServer({
        database: database.connectionString
      });
    } catch (error) {
      // Cleanup partial setup on error
      if (database) {
        await database.stop().catch(() => {});
      }
      throw error;
    }
  });

  beforeEach(async () => {
    // Verify services are still running
    if (!database.isConnected()) {
      throw new Error('Database connection lost');
    }
    
    if (!server.isRunning()) {
      throw new Error('Server is not running');
    }
    
    // Reset to clean state
    await database.reset();
  });

  afterEach(async () => {
    // Always attempt cleanup, even if test failed
    try {
      await database.clearTestData();
    } catch (error) {
      console.warn('Failed to clear test data:', error.message);
    }
  });

  afterAll(async () => {
    // Cleanup in reverse order with error handling
    const cleanupErrors = [];
    
    if (server) {
      try {
        await server.stop();
      } catch (error) {
        cleanupErrors.push(`Server cleanup failed: ${error.message}`);
      }
    }
    
    if (database) {
      try {
        await database.stop();
      } catch (error) {
        cleanupErrors.push(`Database cleanup failed: ${error.message}`);
      }
    }
    
    if (cleanupErrors.length > 0) {
      console.warn('Cleanup warnings:', cleanupErrors.join('; '));
    }
  });

  test('handles normal operations', async () => {
    const response = await makeAPICall('/health');
    expect(response.status).toBe('healthy');
  });
});
  

Timeout and Retry Logic

            
describe('Network-dependent Tests', () => {
  beforeAll(async () => {
    // Wait for external service with timeout
    await waitForService('https://api.external-service.com/health', {
      timeout: 30000,
      retryInterval: 1000
    });
  });

  beforeEach(async () => {
    // Retry flaky setup operations
    await retryOperation(async () => {
      await resetExternalState();
    }, {
      maxAttempts: 3,
      delay: 1000
    });
  });

  test('integrates with external service', async () => {
    const result = await callExternalAPI();
    expect(result).toMatchObject({ success: true });
  });
});

// Helper function for retrying operations
async function retryOperation(operation, options = {}) {
  const { maxAttempts = 3, delay = 1000 } = options;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (attempt === maxAttempts) {
        throw error;
      }
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
  

Advanced Patterns

Shared Test Context

            
describe('E-commerce Integration', () => {
  const testContext = {
    users: {},
    products: {},
    orders: {}
  };

  beforeAll(async () => {
    // Create shared test data
    testContext.users.customer = await createTestUser({
      type: 'customer',
      email: 'customer@test.com'
    });
    
    testContext.users.admin = await createTestUser({
      type: 'admin',
      email: 'admin@test.com'
    });
    
    testContext.products.laptop = await createTestProduct({
      name: 'Test Laptop',
      price: 999.99,
      stock: 10
    });
  });

  describe('Order Management', () => {
    beforeEach(() => {
      // Reset order context for each test
      testContext.orders = {};
    });

    test('customer can place order', async () => {
      const order = await placeOrder({
        userId: testContext.users.customer.id,
        items: [{ 
          productId: testContext.products.laptop.id, 
          quantity: 1 
        }]
      });

      testContext.orders.customerOrder = order;
      
      expect(order).toMatchObject({
        status: 'pending',
        total: 999.99
      });
    });

    test('admin can view all orders', async () => {
      // Use order from previous test context
      const orders = await getOrders({
        userId: testContext.users.admin.id
      });
      
      expect(orders).toBeArray();
      expect(orders.length).toBeGreaterThan(0);
    });
  });

  afterAll(async () => {
    // Cleanup shared resources
    await Promise.all([
      deleteTestUser(testContext.users.customer.id),
      deleteTestUser(testContext.users.admin.id),
      deleteTestProduct(testContext.products.laptop.id)
    ]);
  });
});
  

Conditional Setup Based on Environment

            
describe('Environment-specific Tests', () => {
  beforeAll(async () => {
    if (process.env.NODE_ENV === 'ci') {
      // CI-specific setup
      await setupCIEnvironment();
    } else {
      // Local development setup
      await setupLocalEnvironment();
    }
  });

  beforeEach(async () => {
    // Skip slow setup in CI
    if (process.env.CI !== 'true') {
      await seedLargeDataset();
    }
  });

  test.skipIf(process.env.CI === 'true')('performance test with large dataset', async () => {
    const startTime = Date.now();
    await processLargeDataset();
    const duration = Date.now() - startTime;
    
    expect(duration).toBeLessThan(5000);
  });

  test('basic functionality works everywhere', async () => {
    const result = await basicOperation();
    expect(result).toBeTruthy();
  });
});
  

Best Practices

Hook Execution Order

Execution Order: Hooks execute in a predictable order:
  1. beforeAll (outermost to innermost)
  2. beforeEach (outermost to innermost)
  3. Test execution
  4. afterEach (innermost to outermost)
  5. afterAll (innermost to outermost)

Do's and Don'ts

            
// ✅ Good: Clean state for each test
beforeEach(() => {
  userService.reset();
  mockDatabase.clear();
});

// ❌ Bad: Tests depend on execution order
let userId;
test('creates user', () => {
  userId = userService.create({ name: 'John' });
});
test('finds user', () => {
  const user = userService.find(userId); // Depends on previous test
});

// ✅ Good: Independent tests
describe('User Service', () => {
  let testUserId;
  
  beforeEach(() => {
    testUserId = userService.create({ name: 'John' });
  });
  
  test('finds user by id', () => {
    const user = userService.find(testUserId);
    expect(user.name).toBe('John');
  });
});

// ✅ Good: Async hook handling
beforeAll(async () => {
  await connectToDatabase();
});

// ❌ Bad: Forgetting to await
beforeAll(() => {
  connectToDatabase(); // Returns promise but not awaited
});

// ✅ Good: Error handling in cleanup
afterAll(async () => {
  try {
    await server.stop();
  } catch (error) {
    console.warn('Server cleanup failed:', error);
  }
});
            
          

Performance Considerations

  • Use beforeAll for expensive operations: Database connections, server startup
  • Use beforeEach for state reset: Clearing data, resetting mocks
  • Minimize I/O in beforeEach: Prefer memory operations over disk/network
  • Parallel-safe setup: Ensure hooks work correctly when tests run in parallel
  • Timeout management: Set appropriate timeouts for long-running setup operations

Testing Hook Failures

            
describe('Hook Error Handling', () => {
  test('handles beforeAll failure gracefully', async () => {
    let setupFailed = false;
    
    try {
      await setupThatMightFail();
    } catch (error) {
      setupFailed = true;
      console.warn('Setup failed, using fallback:', error.message);
      await setupFallback();
    }
    
    if (setupFailed) {
      // Adjust test expectations for fallback scenario
      expect(getServiceStatus()).toBe('fallback');
    } else {
      expect(getServiceStatus()).toBe('ready');
    }
  });
});
            
          

Integration with Other Features

Setup and teardown hooks work seamlessly with other Fauji features:

  • Async Testing: All hooks support async/await and promises
  • Mock Functions: Setup and reset mocks in hooks
  • Fake Timers: Enable/disable timer mocking in hooks
  • Custom Matchers: Initialize custom matcher state in hooks

For more advanced testing patterns, see:

Common Pitfalls

  • Forgetting cleanup: Always pair setup with corresponding teardown
  • Order dependencies: Tests should be independent and not rely on execution order
  • Async without await: Always await async operations in hooks
  • Shared mutable state: Avoid sharing mutable objects between tests
  • Ignored errors: Handle errors in cleanup operations gracefully
  • Heavy beforeEach: Avoid expensive operations that run before every test