Skip to main content

Command Palette

Search for a command to run...

How Do Senior Developers Actually Write Tests?

Learn: How Do Senior Developers Actually Write Tests?

Updated
12 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

How Do Senior Developers Actually Write Tests?

Introduction

I'll never forget the day I watched Sarah, our lead developer, delete 3,000 lines of test code. My jaw dropped. We'd spent months building that test suite, and she was just... removing it?

"These tests aren't helping us," she said calmly, scrolling through the file. "They're slowing us down, giving false confidence, and testing the wrong things."

That moment changed how I think about testing forever. You see, I'd been writing tests the way most developers do—mechanically following coverage metrics, testing every single function, and feeling proud when that green checkmark appeared. But senior developers? They approach testing completely differently.

After working alongside seasoned engineers for years and interviewing dozens of tech leads, I've discovered that senior developers don't just write more tests—they write smarter tests. They know which tests matter, which ones waste time, and how to build a test suite that actually catches bugs before users do.

Let me show you exactly how they do it.

The Problem: Why Most Developers Test the Wrong Way

Picture this: You're working on a feature, deadline looming. Your manager asks, "Did you write tests?" You quickly add some unit tests, hit 80% coverage, and call it done. The CI pipeline turns green. You feel accomplished.

Then production breaks.

The bug? It was in the integration between two services—something your unit tests never touched. Or worse, your tests were so tightly coupled to implementation details that a simple refactor broke 47 tests, even though the functionality worked perfectly.

Sound familiar?

Here's the uncomfortable truth: test coverage is a vanity metric. I've seen codebases with 95% coverage that were riddled with bugs, and codebases with 60% coverage that were rock-solid. The difference? Senior developers understand that testing isn't about quantity—it's about strategy.

Most developers fall into these traps:

  • Testing implementation instead of behavior - Your tests break when you refactor, even though nothing changed for users
  • Chasing coverage numbers - Writing tests for getters, setters, and trivial functions that add no value
  • Ignoring integration points - Focusing on unit tests while missing how components work together
  • Writing brittle tests - Tests that fail randomly or require constant maintenance
  • Testing the framework - Verifying that React renders components or Express handles routes (spoiler: they do)

The result? Teams spend more time maintaining tests than writing features, developers start skipping tests to move faster, and bugs still slip through to production.

The Senior Developer Testing Mindset

Before we dive into specific strategies, you need to understand how senior developers think about testing.

Tests Are Documentation That Never Lies

When Sarah reviews code, she reads the tests first. "Tests tell me what the code actually does," she explains. "Comments lie. Documentation gets outdated. But tests? They have to work, or the build fails."

Senior developers write tests that serve as living documentation. When you read their test suite, you understand:

  • What the system does
  • How components interact
  • What edge cases matter
  • Why certain decisions were made

The Testing Pyramid Is Your Friend (But Not a Religion)

You've probably seen the testing pyramid: lots of unit tests at the bottom, fewer integration tests in the middle, and a handful of end-to-end tests at the top.

Senior developers use this as a guideline, not gospel. They adjust the pyramid based on what they're building:

  • API-heavy backend? More integration tests, fewer unit tests
  • Complex business logic? Heavy on unit tests for algorithms
  • User-facing application? More end-to-end tests for critical flows

The key insight: test at the appropriate level. Don't unit test something that only makes sense as an integration.

Test Behavior, Not Implementation

Here's a test I wrote early in my career:

// Bad: Testing implementation
test('UserService.formatName calls toLowerCase and trim', () => {
  const spy1 = jest.spyOn(String.prototype, 'toLowerCase');
  const spy2 = jest.spyOn(String.prototype, 'trim');

  userService.formatName('  JOHN  ');

  expect(spy1).toHaveBeenCalled();
  expect(spy2).toHaveBeenCalled();
});

Here's how a senior developer would write it:

// Good: Testing behavior
test('formatName normalizes user input', () => {
  expect(userService.formatName('  JOHN  ')).toBe('john');
  expect(userService.formatName('Jane')).toBe('jane');
  expect(userService.formatName('  MiXeD CaSe  ')).toBe('mixed case');
});

See the difference? The first test breaks if you refactor the implementation. The second test only cares about the outcome—exactly what users care about.

How Senior Developers Structure Their Test Strategy

1. Start With the Critical Path

Senior developers don't test everything equally. They identify the critical path—the core functionality that, if broken, would be catastrophic.

For an e-commerce site, that's:

  • Users can add items to cart
  • Users can complete checkout
  • Payment processing works
  • Orders are recorded correctly

Everything else is secondary. Sarah taught me to ask: "If this breaks, do we wake someone up at 3 AM?" If yes, it needs comprehensive tests. If no, maybe it doesn't need tests at all.

2. Write Integration Tests for Business Logic

Here's where senior developers diverge from conventional wisdom. They often write integration tests before unit tests.

Why? Because integration tests catch real bugs. They test how your code actually runs in production—with databases, APIs, and all the messy reality of software.

// Integration test for order processing
describe('Order Processing', () => {
  let db, paymentGateway, emailService;

  beforeEach(async () => {
    db = await setupTestDatabase();
    paymentGateway = new MockPaymentGateway();
    emailService = new MockEmailService();
  });

  test('successful order flow', async () => {
    const order = await createOrder({
      userId: 'user123',
      items: [{ productId: 'prod456', quantity: 2 }],
      paymentMethod: 'credit_card'
    });

    // Verify order was created
    const savedOrder = await db.orders.findById(order.id);
    expect(savedOrder.status).toBe('pending');

    // Process payment
    await processPayment(order.id);

    // Verify payment was charged
    expect(paymentGateway.charges).toHaveLength(1);
    expect(paymentGateway.charges[0].amount).toBe(order.total);

    // Verify order status updated
    const processedOrder = await db.orders.findById(order.id);
    expect(processedOrder.status).toBe('confirmed');

    // Verify confirmation email sent
    expect(emailService.sentEmails).toHaveLength(1);
    expect(emailService.sentEmails[0].to).toBe(order.userEmail);
  });
});

This test verifies the entire flow. It catches bugs that unit tests miss—like forgetting to update the order status or failing to send the confirmation email.

3. Unit Test Complex Logic and Edge Cases

Once the integration tests are solid, senior developers add unit tests for:

  • Complex algorithms - Sorting, filtering, calculations
  • Edge cases - Null values, empty arrays, boundary conditions
  • Pure functions - Functions with no side effects that are easy to test
// Unit test for complex pricing logic
describe('calculateDiscount', () => {
  test('applies 10% discount for orders over $100', () => {
    expect(calculateDiscount(150, 'standard')).toBe(15);
  });

  test('applies 20% discount for premium members', () => {
    expect(calculateDiscount(100, 'premium')).toBe(20);
  });

  test('caps discount at 50% of order value', () => {
    expect(calculateDiscount(100, 'super_premium')).toBe(50);
  });

  test('handles zero and negative values', () => {
    expect(calculateDiscount(0, 'standard')).toBe(0);
    expect(calculateDiscount(-50, 'standard')).toBe(0);
  });
});

4. Use End-to-End Tests Sparingly

E2E tests are slow, brittle, and expensive to maintain. Senior developers write them only for:

  • Critical user journeys (signup, checkout, core features)
  • Smoke tests after deployment
  • Features that have broken in production before

They keep E2E tests simple and focused:

// E2E test for checkout flow
test('user can complete purchase', async () => {
  await page.goto('/products');
  await page.click('[data-testid="add-to-cart-123"]');
  await page.click('[data-testid="cart-icon"]');
  await page.click('[data-testid="checkout-button"]');

  await page.fill('[data-testid="card-number"]', '4242424242424242');
  await page.fill('[data-testid="card-expiry"]', '12/25');
  await page.fill('[data-testid="card-cvc"]', '123');

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

  await expect(page.locator('[data-testid="order-confirmation"]')).toBeVisible();
});

Advanced Testing Techniques Senior Developers Use

Test Data Builders

Instead of creating test data inline, senior developers use builders for consistency and readability:

// Without builder - messy and repetitive
test('processes order correctly', () => {
  const order = {
    id: 'order123',
    userId: 'user456',
    items: [{ productId: 'prod789', quantity: 2, price: 29.99 }],
    status: 'pending',
    createdAt: new Date(),
    shippingAddress: { street: '123 Main St', city: 'Boston', zip: '02101' }
  };
  // test code...
});

// With builder - clean and flexible
test('processes order correctly', () => {
  const order = new OrderBuilder()
    .withUser('user456')
    .withItem('prod789', 2)
    .pending()
    .build();
  // test code...
});

Contract Testing for Microservices

When working with microservices, senior developers use contract tests to ensure services can communicate:

// Consumer contract test
describe('Order Service expects User Service to', () => {
  test('return user details with email', async () => {
    const user = await userServiceClient.getUser('user123');

    expect(user).toMatchObject({
      id: expect.any(String),
      email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/),
      name: expect.any(String)
    });
  });
});

Snapshot Testing for UI Components

For React/Vue components, senior developers use snapshot tests to catch unintended changes:

test('renders product card correctly', () => {
  const product = { id: '123', name: 'Widget', price: 29.99 };
  const { container } = render(<ProductCard product={product} />);

  expect(container).toMatchSnapshot();
});

But they're careful—snapshots can become noise if overused. They only snapshot components that shouldn't change often.

Property-Based Testing

For complex logic, senior developers sometimes use property-based testing:

import fc from 'fast-check';

test('sorting is idempotent', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) => {
      const sorted1 = sort(arr);
      const sorted2 = sort(sorted1);
      expect(sorted1).toEqual(sorted2);
    })
  );
});

This generates hundreds of random test cases, catching edge cases you'd never think of.

Testing Anti-Patterns to Avoid

The "Test Everything" Trap

Don't test:

  • Getters and setters
  • Framework code (React, Express, etc.)
  • Third-party libraries
  • Trivial functions with no logic

Your time is valuable. Focus on code that can actually break.

The "Mock Everything" Trap

Over-mocking leads to tests that pass but code that fails in production:

// Bad: Mocking too much
test('saves user', async () => {
  const mockDb = { save: jest.fn().mockResolvedValue(true) };
  const result = await userService.save(user, mockDb);
  expect(result).toBe(true);
});

This test is useless. It only verifies that you called the mock correctly. Use real dependencies when possible, especially databases (use test databases or in-memory alternatives).

The "Flaky Test" Trap

Flaky tests—tests that randomly fail—destroy trust in your test suite. Senior developers fix or delete flaky tests immediately. Common causes:

  • Timing issues - Use proper waits, not arbitrary sleeps
  • Shared state - Ensure tests are isolated
  • External dependencies - Mock unreliable services
  • Random data - Use fixed seeds for random generators

Comparison Table: Junior vs Senior Testing Approach

AspectJunior DeveloperSenior Developer
GoalAchieve high coverage percentageCatch bugs that matter
FocusUnit tests for every functionIntegration tests for critical paths
Test ScopeTests implementation detailsTests behavior and outcomes
MockingMocks everythingMocks only external dependencies
MaintenanceTests break with every refactorTests survive refactoring
SpeedSlow test suite (10+ minutes)Fast feedback (under 2 minutes)
ConfidenceFalse confidence from coverageReal confidence from meaningful tests
DocumentationTests are separate from docsTests serve as living documentation

How to Level Up Your Testing Skills

1. Read Your Team's Tests

The fastest way to learn is to read tests written by senior developers on your team. Notice:

  • What they test vs. what they skip
  • How they structure test files
  • What assertions they use
  • How they handle test data

2. Practice Test-Driven Development (TDD)

I was skeptical of TDD for years. Then I tried it for a month straight. The discipline of writing tests first forces you to think about behavior before implementation.

Start small:

  1. Write a failing test
  2. Write minimal code to pass it
  3. Refactor
  4. Repeat

3. Review Your Test Suite Regularly

Every quarter, Sarah schedules "test hygiene day." The team reviews the test suite and asks:

  • Which tests haven't caught bugs in 6 months?
  • Which tests break constantly?
  • Which tests take forever to run?

Then they delete or improve them. A smaller, focused test suite beats a bloated one every time.

4. Measure What Matters

Instead of coverage, track:

  • Defect escape rate - How many bugs reach production?
  • Test execution time - Can developers run tests frequently?
  • Test maintenance burden - How often do tests need updates?
  • Confidence level - Do developers trust the test suite?

FAQ Section

How much test coverage should I aim for?

Forget the magic number. Instead, ask: "Are my critical paths tested?" If your core business logic has comprehensive tests and you're catching bugs before production, you have enough coverage. That might be 60% or 90%—the number doesn't matter.

Senior developers focus on risk-based testing. High-risk code (payment processing, data deletion, security features) needs thorough testing. Low-risk code (formatting functions, simple getters) might not need tests at all.

Should I write tests before or after writing code?

Both approaches work, but senior developers often write tests first for complex features. Why? It forces you to think about the API and behavior before getting lost in implementation details.

For simple features or exploratory coding, writing tests after is fine. The key is that tests exist before code reaches production.

How do I test code that depends on external APIs?

Senior developers use a layered approach:

  1. Unit tests - Mock the API client completely
  2. Integration tests - Use a fake/stub API server
  3. Contract tests - Verify your assumptions about the API
  4. Smoke tests - Hit the real API in staging

Never hit real external APIs in your test suite—it's slow, unreliable, and can cost money.

What's the best testing framework?

The one your team already uses. Senior developers don't waste time debating Jest vs. Mocha vs. Vitest. They pick one and master it.

That said, modern frameworks like Jest and Vitest offer great developer experience with built-in mocking, coverage, and watch mode.

How do I convince my team to improve our testing?

Start small. Don't try to rewrite the entire test suite. Instead:

  1. Lead by example - Write great tests for your features
  2. Share wins - When your tests catch a bug, celebrate it
  3. Measure impact - Track how testing reduces production bugs
  4. Make it easy - Set up test templates and helpers

Change happens gradually. Focus on improving new code first, then refactor old tests as you touch that code.

Conclusion: Your Action Plan

Here's how to start testing like a senior developer today:

This week:

  • Identify your application's critical path
  • Write one integration test for that path
  • Delete tests that haven't provided value in 6 months

This month:

  • Review your test suite with a colleague
  • Refactor one test file to focus on behavior, not implementation
  • Set up test data builders for common objects

This quarter:

  • Measure your defect escape rate
  • Implement contract testing if you use microservices
  • Establish team testing guidelines

Remember: senior developers don't write perfect tests from day one. They've made every mistake, written terrible tests, and learned from experience. The difference is they've developed a strategy—they know what to test, how to test it, and when to skip testing altogether.

Testing isn't about coverage metrics or following dogma. It's about building confidence that your code works and will keep working as you change it. Start with the critical paths, test behavior over implementation, and keep your test suite lean and fast.

Your future self—the one who isn't debugging production at 2 AM—will thank you.

Now go write some tests that actually matter.