gaveho.
testingtypescriptsoftware-engineering

Testing Isn't Optional: My 12-Year Evolution from Test-Hater to Test-Driven

Apr 30, 2026

I used to ship code without tests and called myself productive. Twelve years later, after debugging production fires at 2 AM, I learned that testing isn't about perfectionism—it's about sleeping well at night. Here's what actually works.

Testing Isn't Optional: My 12-Year Evolution from Test-Hater to Test-Driven

Let me be honest: I shipped my first production feature without a single test. I was young, fast, and thought tests were for people who didn't trust their code. Then I spent a weekend debugging a race condition that a simple integration test would have caught in 30 seconds.

Twelve years and three companies later, my relationship with testing has completely transformed. Not because I became a purist—I'm still pragmatic—but because I learned which tests actually matter.

The Tests That Actually Saved Me

At Snapchat, we had millions of users hitting our APIs. One poorly tested edge case could mean pager duty at 3 AM. Here's what I learned works:

1. Integration Tests Over Unit Tests (Usually)

Unpopular opinion: most unit tests are a waste of time. Testing that your add(a, b) function returns a + b doesn't tell you if your checkout flow works.

I focus on integration tests that verify:

  • API endpoints return the right status codes and data shapes
  • Database transactions commit or roll back correctly
  • Third-party services are called with proper payloads
// This catches real bugs
describe('POST /api/payment', () => {
  it('should create payment and update user balance atomically', async () => {
    const response = await request(app)
      .post('/api/payment')
      .send({ userId: 'test-123', amount: 100 });
    
    expect(response.status).toBe(200);
    const user = await db.users.findById('test-123');
    expect(user.balance).toBe(100);
    const payment = await db.payments.findByUserId('test-123');
    expect(payment.status).toBe('completed');
  });
});

2. Contract Tests for Microservices

At Backbase, we had dozens of microservices talking to each other. When the payments team changed their API response shape, our frontend broke in production.

Contract testing (using tools like Pact or just TypeScript interfaces) ensures both sides agree on the API shape:

// Shared contract
interface PaymentResponse {
  transactionId: string;
  amount: number;
  status: 'pending' | 'completed' | 'failed';
}

// Consumer test validates the contract
it('should return valid PaymentResponse', async () => {
  const response = await paymentService.createPayment({ amount: 100 });
  // TypeScript catches shape mismatches at compile time
  const validated: PaymentResponse = response;
  expect(validated.transactionId).toBeDefined();
});

3. Visual Regression Tests for UI

CSS is chaos. I've shipped pixel-perfect components that looked fine on my MacBook but broke on mobile Safari.

Tools like Percy or Chromatic take screenshots of your components and flag visual changes:

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

test('checkout button renders correctly', async ({ page }) => {
  await page.goto('/checkout');
  await page.screenshot({ path: 'checkout-button.png' });
  // Percy automatically compares against baseline
});

What I Don't Test Anymore

Private methods. If it's not part of the public API, I don't test it directly. Refactoring becomes a nightmare when tests break because you renamed an internal helper.

Third-party library code. React doesn't need your tests. Trust the maintainers or don't use the library.

Trivial getters/setters. If your test is longer than the code it's testing, delete it.

The Real ROI of Testing

Here's what good tests gave me:

  • Confidence to refactor. I rewrote an entire payment flow at 84.51° because the test suite caught every breaking change.
  • Faster code reviews. "Does it have tests?" became the first question. If yes, approval was 10x faster.
  • Better sleep. Deploys on Friday afternoon stopped being scary.

My Current Testing Stack

  • Vitest for unit/integration tests (faster than Jest, works with Vite)
  • Playwright for E2E tests (more reliable than Cypress in my experience)
  • TypeScript as the first line of defense (catches type errors before runtime)
  • Supabase Edge Functions with built-in testing utils
import { createClient } from '@supabase/supabase-js';
import { expect, test } from 'vitest';

test('edge function returns user data', async () => {
  const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
  const { data, error } = await supabase.functions.invoke('get-user', {
    body: { userId: 'test-123' }
  });
  
  expect(error).toBeNull();
  expect(data.user.email).toBe('test@example.com');
});

Start Small, Test What Hurts

You don't need 100% coverage. You need tests for the code that breaks in production.

Start with:

  1. One integration test for your most critical user flow
  2. Contract tests if you have multiple services
  3. A few E2E tests for checkout/signup/login

Then add tests when bugs happen. Each production bug should generate a test that prevents it from happening again.

Testing isn't about perfectionism. It's about making 2 AM debugging sessions rare instead of routine.