Development

When Unit Testing Becomes Essential: A Developer's Journey

Asep Alazhari

Discover why unit testing matters through real production failures and learn when to implement tests in your Next.js projects.

When Unit Testing Becomes Essential: A Developer's Journey

The Wake-Up Call: When Ignoring Tests Costs You

I’ll be honest, I used to think unit testing was a waste of time. Why write tests when I could be shipping features? It felt like doing double the work: first writing the logic, then writing tests for that same logic. My brain was already exhausted from solving complex problems, and the idea of writing tests on top of that? It just felt… redundant.

That mindset worked fine, until it didn’t.

One day, a client requested an enhancement to Feature A, a complex piece of functionality with multiple conditional branches. The requirements seemed straightforward, so I added the new condition, tested it manually (or so I thought), and pushed it to production. Within hours, support tickets started flooding in. The old conditions that had been working perfectly for months? They stopped working. Users couldn’t complete their workflows. The new feature had inadvertently broken existing functionality.

The real kicker? I couldn’t test everything manually. The feature had grown so complex with so many edge cases that manually verifying every scenario would take hours. That’s when it hit me: I needed unit tests, and I needed them yesterday.

Understanding Unit Testing: What It Really Is

Unit testing isn’t about writing tests for every single line of code. It’s about documenting behavior and creating a safety net for your codebase. Think of unit tests as your code’s contract, they define what your functions, components, and modules should do under various circumstances.

A unit test focuses on testing a single “unit” of code in isolation, typically a function, method, or component. The goal is to verify that this unit behaves correctly when given specific inputs or conditions.

// Example: A simple utility function
export function calculateDiscount(price, discountPercent) {
    if (price < 0 || discountPercent < 0) {
        throw new Error("Values must be positive");
    }
    if (discountPercent > 100) {
        throw new Error("Discount cannot exceed 100%");
    }
    return price - (price * discountPercent) / 100;
}

// Unit test for this function
describe("calculateDiscount", () => {
    it("should calculate discount correctly", () => {
        expect(calculateDiscount(100, 20)).toBe(80);
    });

    it("should throw error for negative price", () => {
        expect(() => calculateDiscount(-100, 20)).toThrow("Values must be positive");
    });

    it("should throw error for discount over 100%", () => {
        expect(() => calculateDiscount(100, 150)).toThrow("Discount cannot exceed 100%");
    });
});

Also Read: Server Actions vs Client Rendering in Next.js: The 2025 Guide

When Unit Testing Becomes Non-Negotiable

Through painful experience, I’ve identified specific scenarios where unit testing isn’t just helpful, it’s essential:

1. Complex Business Logic

If your code has multiple if-else statements, switch cases, or nested conditions, unit tests are your friend. These are the exact scenarios where adding one condition can break another. Feature A in my story? It had at least seven different conditional paths. Without tests, I had no way to verify that all seven still worked after my changes.

2. Client-Facing Components with Unique Behaviors

In my Next.js projects, I focus heavily on testing Client Components because they often contain:

  • Form validation logic
  • Conditional rendering based on user state
  • Interactive elements with various states (loading, error, success)
  • Complex user interactions
// Example: Testing a complex client component
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { OrderForm } from './OrderForm';

describe('OrderForm', () => {
  it('should show validation errors for empty fields', async () => {
    render(<OrderForm />);

    const submitButton = screen.getByRole('button', { name: /submit/i });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(screen.getByText(/name is required/i)).toBeInTheDocument();
      expect(screen.getByText(/email is required/i)).toBeInTheDocument();
    });
  });

  it('should disable submit button during submission', async () => {
    render(<OrderForm />);

    fireEvent.change(screen.getByLabelText(/name/i), {
      target: { value: 'John Doe' }
    });
    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: '[email protected]' }
    });

    const submitButton = screen.getByRole('button', { name: /submit/i });
    fireEvent.click(submitButton);

    expect(submitButton).toBeDisabled();
  });
});

3. Shared Utilities and Helper Functions

Any function used across multiple parts of your application should have unit tests. These are high-leverage tests because a bug in a shared utility can cascade throughout your entire application.

4. Before Refactoring Legacy Code

Planning to refactor a gnarly old function? Write tests for its current behavior first. This gives you confidence that your refactored version maintains the same functionality.

Also Read: Complete Guide to Password Validation in React with Chakra UI and React Hook Form

My Current Testing Stack: Jest, Testing Library & jsdom

For my Next.js projects, I use a combination that has proven reliable:

  • Jest: The test runner and assertion library
  • React Testing Library: For component testing with a user-centric approach
  • jsdom: Simulates a DOM environment in Node.js

This stack integrates seamlessly with Next.js and encourages testing from the user’s perspective rather than implementation details.

// jest.config.js
const nextJest = require("next/jest");

const createJestConfig = nextJest({
    dir: "./",
});

const customJestConfig = {
    setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
    testEnvironment: "jsdom",
    moduleNameMapper: {
        "^@/components/(.*)$": "<rootDir>/components/$1",
        "^@/lib/(.*)$": "<rootDir>/lib/$1",
    },
};

module.exports = createJestConfig(customJestConfig);

The Real ROI of Unit Testing

Here’s what changed after I committed to unit testing:

  1. Confidence in Changes: I can refactor with confidence knowing tests will catch any regressions.
  2. Faster Debugging: When a test fails, it pinpoints exactly where the problem is.
  3. Better Code Design: Writing testable code naturally leads to better architecture, smaller functions, clearer separation of concerns.
  4. Documentation: Tests serve as living documentation showing how code should be used.
  5. Fewer Production Bugs: Catching issues in development is exponentially cheaper than fixing them in production.

When You Can Skip Unit Tests

Not everything needs unit tests. Here’s where I consciously skip them:

  • Simple presentational components with no logic
  • Configuration files
  • Trivial getter/setter functions
  • Code that’s primarily glue (connecting other well-tested units)

The key is intentionality. Skip tests deliberately, not out of laziness.

Starting Your Testing Journey

If you’re where I was, skeptical about testing, here’s my advice:

  1. Start small: Pick one complex function and write tests for it
  2. Test new code: Make it a rule that new features come with tests
  3. Test before fixing bugs: When you find a bug, write a failing test first, then fix it
  4. Measure coverage: Use tools to see which code lacks tests, but don’t obsess over 100% coverage
# Run tests with coverage report
npm test -- --coverage

# Run tests in watch mode during development
npm test -- --watch

The Bottom Line

Unit testing isn’t about doubling your work, it’s about investing in quality and stability. That production incident taught me an expensive lesson: the time I “saved” by skipping tests was nothing compared to the time spent debugging, apologizing to clients, and rushing hotfixes.

Now, when I write a complex conditional or a client-facing component, I write tests alongside it. It’s become part of my workflow, not a separate chore. And honestly? I sleep better knowing that my code has a safety net.

Your future self, and your clients, will thank you for it.

Back to Blog

Related Posts

View All Posts »