Back to Blog

Software Testing Strategies: Unit, Integration, E2E, and What the Testing Trophy Actually Means

Software testing strategies in 2026 — unit vs integration vs E2E tests, the testing trophy, Jest, Playwright, pytest, test coverage, and how high-performing tea

Viprasol Tech Team
April 13, 2026
13 min read

Software Testing Strategies: Unit, Integration, E2E, and What the Testing Trophy Actually Means

Most discussions of software testing start with the testing pyramid — lots of unit tests, fewer integration tests, a handful of E2E tests. The problem with the pyramid is that it under-weights integration tests, which provide more confidence per dollar than unit tests for most web applications.

Kent C. Dodds' testing trophy (static → unit → integration → E2E) is a better model: integration tests are the widest layer because they test the behavior that actually matters to users without the brittleness and maintenance overhead of full E2E tests.

This guide covers practical testing strategy — what to test at each layer, which tools to use, and the habits that turn testing from a chore into a productivity multiplier.


The Testing Trophy

         ╭──────────────╮
         │     E2E      │  (few, high-value critical paths)
        ╭┴──────────────┴╮
        │  Integration   │  (most tests — widest layer)
       ╭┴────────────────┴╮
       │      Unit        │  (fast, isolated logic)
      ╭┴──────────────────┴╮
      │   Static Analysis  │  (TypeScript, ESLint — free coverage)
      ╰────────────────────╯

Static analysis (TypeScript, ESLint, Zod) catches bugs at zero runtime cost. Enable strict TypeScript and treat type errors as test failures.

Unit tests test isolated functions and classes. Best for: pure functions, complex algorithms, business logic with many branches.

Integration tests test a unit of behavior from the outside — typically an HTTP request through to the database response. Test behavior, not implementation.

E2E tests drive a real browser against a real (or staging) environment. Reserve for the most critical user journeys: signup flow, checkout, core product action.


Unit Testing (TypeScript + Jest)

// src/utils/pricing.ts
export function calculateDiscount(
  price: number,
  discountCode: string,
  userTier: 'free' | 'pro' | 'enterprise'
): number {
  if (price <= 0) throw new Error('Price must be positive');
  
  const tierDiscounts: Record<string, number> = {
    free: 0,
    pro: 0.1,
    enterprise: 0.2,
  };

  const codeDiscounts: Record<string, number> = {
    'SAVE10': 0.1,
    'SAVE20': 0.2,
    'LAUNCH50': 0.5,
  };

  const tierDiscount = tierDiscounts[userTier] ?? 0;
  const codeDiscount = codeDiscounts[discountCode] ?? 0;
  
  // Discounts stack additively, capped at 60%
  const totalDiscount = Math.min(tierDiscount + codeDiscount, 0.6);
  return Math.round(price * (1 - totalDiscount) * 100) / 100;
}
// src/utils/pricing.test.ts
import { calculateDiscount } from './pricing';

describe('calculateDiscount', () => {
  it('applies tier discount for pro users', () => {
    expect(calculateDiscount(100, '', 'pro')).toBe(90);
  });

  it('applies code discount for free users', () => {
    expect(calculateDiscount(100, 'SAVE10', 'free')).toBe(90);
  });

  it('stacks tier and code discounts', () => {
    expect(calculateDiscount(100, 'SAVE10', 'pro')).toBe(80); // 10% + 10%
  });

  it('caps combined discount at 60%', () => {
    expect(calculateDiscount(100, 'LAUNCH50', 'enterprise')).toBe(40); // 50% + 20% = 70% → capped at 60%
  });

  it('returns original price for unknown code', () => {
    expect(calculateDiscount(100, 'INVALID', 'free')).toBe(100);
  });

  it('throws on non-positive price', () => {
    expect(() => calculateDiscount(0, '', 'free')).toThrow('Price must be positive');
    expect(() => calculateDiscount(-10, '', 'free')).toThrow('Price must be positive');
  });

  it('handles fractional prices', () => {
    expect(calculateDiscount(9.99, 'SAVE10', 'free')).toBe(8.99);
  });
});

Unit test principles:

  • Test behavior, not implementation — don't test that a private method was called
  • One assertion concept per test (can have multiple expect calls for the same thing)
  • Arrange-Act-Assert structure: set up → execute → verify
  • Name tests as sentences: "returns X when Y"

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Integration Testing (HTTP + Database)

Integration tests start the real application (or a close equivalent), make HTTP requests, and verify responses — including what was written to the database.

// src/__tests__/orders.integration.test.ts
import request from 'supertest';
import { app } from '../app';
import { db } from '../db';

// Reset database state before each test
beforeEach(async () => {
  await db('orders').delete();
  await db('order_items').delete();
});

afterAll(async () => {
  await db.destroy(); // Close DB connection pool
});

describe('POST /api/orders', () => {
  it('creates an order and returns 201 with order details', async () => {
    const { token } = await createTestUser({ plan: 'pro' });
    
    const response = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${token}`)
      .send({
        items: [
          { productId: 'prod-123', quantity: 2 },
          { productId: 'prod-456', quantity: 1 },
        ],
        shippingAddress: {
          line1: '123 Main St',
          city: 'New York',
          country: 'US',
          postalCode: '10001',
        },
      });

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject({
      id: expect.any(String),
      status: 'pending',
      total: expect.any(Number),
      items: expect.arrayContaining([
        expect.objectContaining({ productId: 'prod-123', quantity: 2 }),
      ]),
    });

    // Verify the order was actually persisted
    const dbOrder = await db('orders').where({ id: response.body.id }).first();
    expect(dbOrder).toBeDefined();
    expect(dbOrder.status).toBe('pending');
  });

  it('returns 400 when items array is empty', async () => {
    const { token } = await createTestUser();
    
    const response = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${token}`)
      .send({ items: [] });

    expect(response.status).toBe(400);
    expect(response.body.error).toMatch(/items/i);
  });

  it('returns 401 without authentication', async () => {
    const response = await request(app)
      .post('/api/orders')
      .send({ items: [{ productId: 'prod-123', quantity: 1 }] });

    expect(response.status).toBe(401);
  });
});

Test Database Setup

// jest.setup.ts — runs before test suite
import { execSync } from 'child_process';

// Use a separate test database
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL 
  ?? 'postgresql://test:test@localhost:5432/testdb';

beforeAll(async () => {
  // Run migrations on test database
  execSync('npm run db:migrate', { env: process.env });
});
// package.json
{
  "scripts": {
    "test": "jest",
    "test:unit": "jest --testPathPattern='\\.unit\\.test'",
    "test:integration": "jest --testPathPattern='\\.integration\\.test'",
    "test:watch": "jest --watch"
  },
  "jest": {
    "testEnvironment": "node",
    "setupFilesAfterFramework": ["./jest.setup.ts"],
    "coverageThreshold": {
      "global": {
        "branches": 70,
        "functions": 80,
        "lines": 80
      }
    }
  }
}

E2E Testing (Playwright)

Playwright tests drive a real browser. Use sparingly — for the highest-value user journeys only.

// tests/e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Checkout Flow', () => {
  test('user can complete a purchase', async ({ page }) => {
    // Login
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'test@example.com');
    await page.fill('[data-testid="password"]', 'TestPass123!');
    await page.click('[data-testid="login-button"]');
    await expect(page).toHaveURL('/dashboard');

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

    // Proceed to checkout
    await page.goto('/checkout');
    await expect(page.locator('[data-testid="cart-total"]')).toBeVisible();

    // Enter payment details (use Stripe test card)
    const stripeFrame = page.frameLocator('iframe[title*="Secure card number"]');
    await stripeFrame.locator('[placeholder="Card number"]').fill('4242424242424242');
    await stripeFrame.locator('[placeholder="MM / YY"]').fill('12/28');
    await stripeFrame.locator('[placeholder="CVC"]').fill('123');

    await page.click('[data-testid="place-order-button"]');

    // Verify order confirmation
    await expect(page).toHaveURL(/\/orders\/[a-z0-9-]+\/confirmation/);
    await expect(page.locator('[data-testid="order-confirmation"]')).toBeVisible();
    await expect(page.locator('[data-testid="confirmation-email"]')).toContainText('test@example.com');
  });
});
# playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,  // Retry flaky tests in CI
  workers: process.env.CI ? 2 : undefined,
  
  use: {
    baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure',
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
  ],
});

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Python Testing (pytest)

# tests/test_pricing.py
import pytest
from app.utils.pricing import calculate_discount

class TestCalculateDiscount:
    def test_applies_tier_discount_for_pro(self):
        assert calculate_discount(100.0, '', 'pro') == 90.0

    def test_stacks_discounts(self):
        assert calculate_discount(100.0, 'SAVE10', 'pro') == 80.0

    def test_caps_discount_at_60_percent(self):
        assert calculate_discount(100.0, 'LAUNCH50', 'enterprise') == 40.0

    @pytest.mark.parametrize("price", [0, -10, -0.01])
    def test_raises_on_non_positive_price(self, price):
        with pytest.raises(ValueError, match="positive"):
            calculate_discount(price, '', 'free')


# tests/test_api_orders.py — integration test
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.db import get_db

@pytest.fixture(autouse=True)
def clean_db():
    """Reset test database before each test."""
    db = next(get_db())
    db.execute("DELETE FROM orders")
    db.commit()
    yield
    db.close()

client = TestClient(app)

def test_create_order_returns_201():
    token = create_test_user_token(plan='pro')
    
    response = client.post(
        "/api/orders",
        json={"items": [{"product_id": "prod-123", "quantity": 2}]},
        headers={"Authorization": f"Bearer {token}"},
    )
    
    assert response.status_code == 201
    data = response.json()
    assert data["status"] == "pending"
    assert len(data["items"]) == 1

def test_create_order_requires_auth():
    response = client.post("/api/orders", json={"items": []})
    assert response.status_code == 401

Coverage: What's Useful, What's Not

Coverage is a signal, not a goal. 100% coverage doesn't mean your tests are good — it just means every line was executed during testing.

Useful coverage targets:

  • Business logic modules: 90%+ branch coverage
  • API handlers: 80%+ line coverage
  • Utility functions: 95%+ (they're easy to test)
  • Database migrations: 0% (no unit tests needed — tested by running them)
  • UI components: 60%+ for critical components

Coverage anti-patterns:

  • Writing trivial tests purely to hit a coverage number
  • Testing implementation details (which function was called)
  • Treating coverage/lcov-report/index.html as a report card
# Generate coverage report
npm test -- --coverage --coverageReporters=text,lcov

# Open HTML report
open coverage/lcov-report/index.html

# CI: fail if coverage drops below threshold
# (configure in jest.config.ts → coverageThreshold)

Testing Cost and ROI

Testing investment that pays off quickly:

  • Integration tests for API endpoints: catch regressions before they reach users
  • Unit tests for complex business logic: catch edge cases in pricing, validation, calculations
  • E2E tests for critical paths: signup, payment, core product action

Testing that has low ROI:

  • Unit tests for CRUD operations with no logic
  • E2E tests that duplicate integration test coverage
  • Snapshot tests for dynamic content

Build time budget: unit tests should run in < 30 seconds; integration tests < 3 minutes; E2E < 10 minutes.


Working With Viprasol

We implement testing strategies for engineering teams — from initial test suite setup through CI integration, coverage baseline establishment, and ongoing testing culture coaching.

Testing strategy consultation →
Software Development Services →
Software Testing Company →


Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.