...
...
#testing #quality assurance #unit testing #integration testing #e2e

Enterprise Testing Strategies: Practical Guide 2025

Enterprise testing strategies: unit, integration & E2E testing. Test automation, coverage, and quality assurance best practices.

V
VooStack Team
October 2, 2025
14 min read

Testing Strategies for Enterprise Applications: A Practical Guide

Testing is where good intentions meet reality. Everyone agrees tests are important. Few teams do it well.

I’ve seen teams with 95% code coverage and broken production deployments. I’ve seen teams with 40% coverage and rock-solid reliability. Coverage percentage doesn’t matter—what you test and how you test it does.

Let me show you a testing strategy that actually works.

The Testing Pyramid (and Why It’s Not Enough)

You’ve seen the pyramid: lots of unit tests, some integration tests, few E2E tests. It’s a good starting point but misses critical context.

The Real Testing Strategy

Unit Tests (70%):

  • Test isolated functions and components
  • Fast, cheap, easy to maintain
  • Catch logic errors
  • Give you confidence to refactor

Integration Tests (20%):

  • Test how components work together
  • Catch interface mismatches
  • Verify database queries
  • Test API contracts

E2E Tests (10%):

  • Test critical user paths
  • Verify the whole system works
  • Catch UI/UX issues
  • Expensive, slow, flaky

The Catch: These percentages shift based on your application. API backend? More integration tests. UI-heavy app? More E2E tests. Functional library? More unit tests.

Unit Testing: The Foundation

Unit tests should be fast, isolated, and test one thing.

What to Unit Test

Pure Functions (always test these):

// Pure function - easy to test
export function calculateDiscount(price: number, discountPercent: number): number {
  if (price < 0 || discountPercent < 0 || discountPercent > 100) {
    throw new Error('Invalid input');
  }
  return price * (1 - discountPercent / 100);
}

// Test
describe('calculateDiscount', () => {
  it('applies discount correctly', () => {
    expect(calculateDiscount(100, 20)).toBe(80);
    expect(calculateDiscount(50, 10)).toBe(45);
  });

  it('throws on invalid input', () => {
    expect(() => calculateDiscount(-10, 20)).toThrow('Invalid input');
    expect(() => calculateDiscount(100, 150)).toThrow('Invalid input');
  });

  it('handles edge cases', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
    expect(calculateDiscount(100, 100)).toBe(0);
  });
});

Business Logic (critical to test):

export class OrderService {
  canCancelOrder(order: Order): boolean {
    const hoursSinceOrder = (Date.now() - order.createdAt.getTime()) / (1000 * 60 * 60);

    if (order.status === 'shipped') return false;
    if (order.status === 'cancelled') return false;
    if (hoursSinceOrder > 24) return false;

    return true;
  }
}

// Test
describe('OrderService', () => {
  describe('canCancelOrder', () => {
    it('allows cancellation within 24 hours', () => {
      const order = {
        status: 'pending',
        createdAt: new Date(Date.now() - 1000 * 60 * 60 * 12), // 12 hours ago
      };

      expect(service.canCancelOrder(order)).toBe(true);
    });

    it('prevents cancellation after 24 hours', () => {
      const order = {
        status: 'pending',
        createdAt: new Date(Date.now() - 1000 * 60 * 60 * 25), // 25 hours ago
      };

      expect(service.canCancelOrder(order)).toBe(false);
    });

    it('prevents cancellation of shipped orders', () => {
      const order = {
        status: 'shipped',
        createdAt: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago
      };

      expect(service.canCancelOrder(order)).toBe(false);
    });
  });
});

React Components (test behavior, not implementation):

import { render, screen, fireEvent } from '@testing-library/react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <span data-testid="count">{count}</span>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

describe('Counter', () => {
  it('starts at zero', () => {
    render(<Counter />);
    expect(screen.getByTestId('count')).toHaveTextContent('0');
  });

  it('increments count', () => {
    render(<Counter />);
    fireEvent.click(screen.getByText('Increment'));
    expect(screen.getByTestId('count')).toHaveTextContent('1');
  });

  it('resets count', () => {
    render(<Counter />);
    fireEvent.click(screen.getByText('Increment'));
    fireEvent.click(screen.getByText('Increment'));
    fireEvent.click(screen.getByText('Reset'));
    expect(screen.getByTestId('count')).toHaveTextContent('0');
  });
});

What NOT to Unit Test

Don’t test the framework:

// Bad - testing React, not your code
it('renders a div', () => {
  const { container } = render(<MyComponent />);
  expect(container.querySelector('div')).toBeInTheDocument();
});

// Good - testing your component's behavior
it('displays user name', () => {
  render(<UserProfile name="Alice" />);
  expect(screen.getByText('Alice')).toBeInTheDocument();
});

Don’t test implementation details:

// Bad - tests implementation
it('calls useState', () => {
  const useStateSpy = jest.spyOn(React, 'useState');
  render(<Counter />);
  expect(useStateSpy).toHaveBeenCalled();
});

// Good - tests behavior
it('updates count on click', () => {
  render(<Counter />);
  fireEvent.click(screen.getByText('Increment'));
  expect(screen.getByTestId('count')).toHaveTextContent('1');
});

Integration Testing: Where Components Meet

Integration tests verify that different parts of your system work together.

API Integration Tests

import { app } from '../src/app';
import request from 'supertest';
import { db } from '../src/database';

describe('POST /api/orders', () => {
  beforeEach(async () => {
    await db.clean(); // Reset database
  });

  it('creates an order', async () => {
    const user = await db.users.create({ email: 'test@example.com' });
    const product = await db.products.create({ name: 'Test Product', price: 100 });

    const response = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${user.token}`)
      .send({
        items: [{ productId: product.id, quantity: 2 }],
      });

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject({
      userId: user.id,
      items: [{ productId: product.id, quantity: 2 }],
      total: 200,
    });

    // Verify database
    const order = await db.orders.findById(response.body.id);
    expect(order).toBeDefined();
  });

  it('rejects invalid orders', async () => {
    const user = await db.users.create({ email: 'test@example.com' });

    const response = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${user.token}`)
      .send({
        items: [], // Empty items
      });

    expect(response.status).toBe(400);
    expect(response.body.error).toContain('items');
  });
});

Database Integration Tests

describe('UserRepository', () => {
  let repo: UserRepository;

  beforeEach(async () => {
    await db.migrate.latest();
    repo = new UserRepository(db);
  });

  afterEach(async () => {
    await db.migrate.rollback();
  });

  it('creates and retrieves users', async () => {
    const user = await repo.create({
      email: 'test@example.com',
      name: 'Test User',
    });

    expect(user.id).toBeDefined();

    const retrieved = await repo.findById(user.id);
    expect(retrieved).toMatchObject({
      email: 'test@example.com',
      name: 'Test User',
    });
  });

  it('finds user by email', async () => {
    await repo.create({ email: 'alice@example.com', name: 'Alice' });
    await repo.create({ email: 'bob@example.com', name: 'Bob' });

    const user = await repo.findByEmail('alice@example.com');
    expect(user?.name).toBe('Alice');
  });

  it('handles duplicate emails', async () => {
    await repo.create({ email: 'test@example.com', name: 'User 1' });

    await expect(
      repo.create({ email: 'test@example.com', name: 'User 2' })
    ).rejects.toThrow('Email already exists');
  });
});

Service Integration Tests

describe('PaymentService', () => {
  let service: PaymentService;
  let mockStripe: jest.Mocked<Stripe>;

  beforeEach(() => {
    mockStripe = {
      charges: {
        create: jest.fn(),
      },
    } as any;

    service = new PaymentService(mockStripe);
  });

  it('processes payment successfully', async () => {
    mockStripe.charges.create.mockResolvedValue({
      id: 'ch_123',
      status: 'succeeded',
      amount: 5000,
    } as any);

    const result = await service.processPayment({
      amount: 5000,
      currency: 'usd',
      source: 'tok_visa',
    });

    expect(result.success).toBe(true);
    expect(result.transactionId).toBe('ch_123');
    expect(mockStripe.charges.create).toHaveBeenCalledWith({
      amount: 5000,
      currency: 'usd',
      source: 'tok_visa',
    });
  });

  it('handles payment failures', async () => {
    mockStripe.charges.create.mockRejectedValue(
      new Error('Card declined')
    );

    const result = await service.processPayment({
      amount: 5000,
      currency: 'usd',
      source: 'tok_chargeDeclined',
    });

    expect(result.success).toBe(false);
    expect(result.error).toBe('Card declined');
  });
});

End-to-End Testing: The Full Journey

E2E tests simulate real user interactions.

Playwright Example

import { test, expect } from '@playwright/test';

test.describe('Purchase flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://localhost:3000');
  });

  test('complete purchase', async ({ page }) => {
    // Search for product
    await page.fill('[data-testid="search-input"]', 'laptop');
    await page.click('[data-testid="search-button"]');

    // Select product
    await page.click('[data-testid="product-0"]');
    await expect(page.locator('h1')).toContainText('MacBook Pro');

    // Add to cart
    await page.click('[data-testid="add-to-cart"]');
    await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');

    // Proceed to checkout
    await page.click('[data-testid="cart-button"]');
    await page.click('[data-testid="checkout-button"]');

    // Fill checkout form
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="cardNumber"]', '4242424242424242');
    await page.fill('[name="expiry"]', '12/25');
    await page.fill('[name="cvc"]', '123');

    // Complete purchase
    await page.click('[data-testid="submit-order"]');

    // Verify confirmation
    await expect(page.locator('h1')).toContainText('Order Confirmed');
    await expect(page.locator('[data-testid="order-number"]')).toBeVisible();
  });

  test('handles out of stock', async ({ page }) => {
    await page.goto('https://localhost:3000/products/out-of-stock-item');

    await expect(page.locator('[data-testid="add-to-cart"]')).toBeDisabled();
    await expect(page.locator('[data-testid="stock-status"]')).toHaveText('Out of Stock');
  });
});

Cypress Example

describe('User Authentication', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('logs in successfully', () => {
    cy.get('[data-testid="email"]').type('user@example.com');
    cy.get('[data-testid="password"]').type('password123');
    cy.get('[data-testid="login-button"]').click();

    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="user-name"]').should('contain', 'John Doe');
  });

  it('shows error for invalid credentials', () => {
    cy.get('[data-testid="email"]').type('user@example.com');
    cy.get('[data-testid="password"]').type('wrongpassword');
    cy.get('[data-testid="login-button"]').click();

    cy.get('[data-testid="error-message"]').should('contain', 'Invalid credentials');
  });

  it('persists login after refresh', () => {
    cy.login('user@example.com', 'password123'); // Custom command

    cy.reload();

    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="user-name"]').should('contain', 'John Doe');
  });
});

// cypress/support/commands.ts
Cypress.Commands.add('login', (email: string, password: string) => {
  cy.request('POST', '/api/auth/login', { email, password })
    .then((response) => {
      localStorage.setItem('token', response.body.token);
    });
});

Test Data Management

Factories

import { faker } from '@faker-js/faker';

export const UserFactory = {
  build: (overrides?: Partial<User>): User => ({
    id: faker.string.uuid(),
    email: faker.internet.email(),
    name: faker.person.fullName(),
    createdAt: faker.date.past(),
    ...overrides,
  }),

  buildMany: (count: number, overrides?: Partial<User>): User[] => {
    return Array.from({ length: count }, () => UserFactory.build(overrides));
  },
};

// Usage
describe('UserService', () => {
  it('filters active users', () => {
    const users = [
      UserFactory.build({ active: true }),
      UserFactory.build({ active: false }),
      UserFactory.build({ active: true }),
    ];

    const active = service.filterActive(users);
    expect(active).toHaveLength(2);
  });
});

Database Fixtures

// fixtures/users.ts
export const users = [
  {
    id: '1',
    email: 'admin@example.com',
    role: 'admin',
  },
  {
    id: '2',
    email: 'user@example.com',
    role: 'user',
  },
];

// In tests
beforeEach(async () => {
  await db.seed.run({ specific: 'users.ts' });
});

Mocking Strategies

When to Mock

External APIs (always mock):

import nock from 'nock';

describe('WeatherService', () => {
  it('fetches weather data', async () => {
    nock('https://api.weather.com')
      .get('/current')
      .query({ city: 'London' })
      .reply(200, {
        temperature: 15,
        conditions: 'Cloudy',
      });

    const weather = await service.getWeather('London');
    expect(weather.temperature).toBe(15);
  });
});

Time (mock for predictable tests):

import { jest } from '@jest/globals';

describe('SubscriptionService', () => {
  beforeEach(() => {
    jest.useFakeTimers();
    jest.setSystemTime(new Date('2025-01-01'));
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('detects expired subscriptions', () => {
    const subscription = {
      expiresAt: new Date('2024-12-31'),
    };

    expect(service.isExpired(subscription)).toBe(true);
  });
});

Database (mock for unit tests, use real for integration):

// Unit test - mock database
const mockRepo = {
  findById: jest.fn().mockResolvedValue({ id: '123', name: 'Test' }),
};

const service = new UserService(mockRepo);

// Integration test - real database
const repo = new UserRepository(db);
const service = new UserService(repo);

When NOT to Mock

Don’t mock what you’re testing:

// Bad - mocking the thing you're testing
it('creates user', () => {
  const mockCreate = jest.fn().mockResolvedValue({ id: '123' });
  service.create = mockCreate;

  service.create({ name: 'Alice' });
  expect(mockCreate).toHaveBeenCalled(); // Meaningless test
});

// Good - test the real function
it('creates user', async () => {
  const user = await service.create({ name: 'Alice' });
  expect(user.name).toBe('Alice');
  expect(user.id).toBeDefined();
});

Performance Testing

Load Testing

import autocannon from 'autocannon';

describe('API Performance', () => {
  it('handles 1000 req/sec', async () => {
    const result = await autocannon({
      url: 'http://localhost:3000/api/products',
      connections: 100,
      duration: 10, // seconds
    });

    expect(result.requests.average).toBeGreaterThan(1000);
    expect(result.latency.p99).toBeLessThan(100); // 99th percentile < 100ms
  });
});

Memory Leak Detection

describe('Memory leaks', () => {
  it('does not leak memory on repeated calls', async () => {
    const initialMemory = process.memoryUsage().heapUsed;

    for (let i = 0; i < 10000; i++) {
      await service.processData({ /* data */ });
    }

    // Force garbage collection
    if (global.gc) global.gc();

    const finalMemory = process.memoryUsage().heapUsed;
    const increase = finalMemory - initialMemory;

    // Memory increase should be minimal
    expect(increase).toBeLessThan(10 * 1024 * 1024); // < 10MB
  });
});

Test Organization

Arrange-Act-Assert Pattern

describe('OrderService', () => {
  it('applies bulk discount', () => {
    // Arrange
    const items = [
      { price: 100, quantity: 10 },
      { price: 50, quantity: 5 },
    ];

    // Act
    const total = service.calculateTotal(items);

    // Assert
    expect(total).toBe(1125); // 10% bulk discount applied
  });
});

Descriptive Test Names

// Bad - vague
it('works', () => { /* ... */ });

// Good - specific
it('applies 10% discount for orders over $1000', () => { /* ... */ });

// Better - full sentence
it('should apply 10% discount when order total exceeds $1000', () => { /* ... */ });

CI/CD Integration

GitHub Actions

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: testpass
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run test:unit
      - run: npm run test:integration

      - name: Upload Coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json

Common Testing Pitfalls

Over-Mocking

// Bad - too many mocks
const mockDb = { query: jest.fn() };
const mockCache = { get: jest.fn() };
const mockLogger = { log: jest.fn() };
const mockQueue = { add: jest.fn() };
// ... testing nothing real

// Good - mock only external dependencies
const service = new UserService(realDb);

Testing Implementation Details

// Bad
it('uses reduce internally', () => {
  const reduceSpy = jest.spyOn(Array.prototype, 'reduce');
  calculateTotal(items);
  expect(reduceSpy).toHaveBeenCalled();
});

// Good
it('calculates total correctly', () => {
  expect(calculateTotal(items)).toBe(150);
});

Flaky Tests

// Bad - race condition
it('updates after delay', async () => {
  service.updateAsync();
  await sleep(100); // Might not be enough
  expect(service.value).toBe(10);
});

// Good - wait for actual condition
it('updates after delay', async () => {
  service.updateAsync();
  await waitFor(() => expect(service.value).toBe(10));
});

Coverage Targets

Don’t aim for 100% coverage. Aim for testing what matters:

Critical code: 100% coverage

  • Payment processing
  • Authentication
  • Data migrations
  • Security logic

Business logic: 90% coverage

  • Order processing
  • Inventory management
  • User workflows

UI components: 70% coverage

  • Test user interactions
  • Test error states
  • Skip trivial rendering

Generated code: 0% coverage

  • Don’t test auto-generated code
  • Don’t test third-party libraries

Conclusion

Good testing isn’t about coverage percentage—it’s about confidence. Test the things that matter: business logic, critical paths, and edge cases.

Start with unit tests for your logic. Add integration tests for your APIs and database. Use E2E tests sparingly for critical user journeys.

And remember: the best test is the one that catches a bug before it reaches production. The second-best test is the one that makes refactoring safe. Everything else is noise.

Need help implementing a robust testing strategy? VooStack helps teams build comprehensive test suites that give real confidence. Let’s talk about your testing needs.

Topics

testing quality assurance unit testing integration testing e2e
V

Written by VooStack Team

Contact author

Share this article