Skip to main content
Back to all posts

Creating Bulletproof Tests Without Mocks for UI Components

In the ever-evolving landscape of software development, ensuring robust and reliable test suites is pivotal. Recently, a significant enhancement was made to our project’s testing strategy: the creation of bulletproof tests for UI components without relying on mocks. This blog post delves into the purpose, implementation, and impact of these changes.

Purpose and Context

The primary motivation behind this initiative was to address persistent issues with mock-dependent tests that were causing reliability concerns, such as hanging and inconsistent test outcomes. The complexity arose when my Claude-powered AI agent created overly sophisticated tests that, while comprehensive, introduced brittleness and maintenance headaches. These AI-generated tests relied heavily on mocks and complex setups that made debugging nearly impossible.

The turning point came when I encountered persistent data fetching issues that highlighted the fragility of our mock-heavy approach. Tests would hang indefinitely, fail intermittently, or pass locally but fail in CI. After struggling to fix these issues, I realized we needed a fundamental shift in our testing philosophy.

By replacing these problematic tests with new bulletproof unit tests, we achieved comprehensive coverage for critical components like home, navigation, and layout. This change was guided by the Bulletproof Testing Guidelines I established, which emphasize test isolation, speed, and reliability over complex mock scenarios.

Technical Implementation Details

Removal of Problematic Tests

The journey began with the elimination of two troublesome test files: auth-redirect.test.tsx and login-functionality.test.tsx. These tests, originally generated by AI assistance, were notorious for their dependency on external mocks, which often led to unpredictable behavior and test hangs. The AI had created tests that tried to mock entire authentication flows, database connections, and API responses - creating a house of cards that would collapse at the slightest change.

Creation of New Test Files

I introduced three new test files that focus on isolated unit testing for the home, navigation, and layout components. The new tests adhere to bulletproof testing principles by emphasizing the following:

  • Isolation: Tests are designed to run fast and independently, without relying on external mocks.
  • Clarity: Tests are concise and focused on verifying UI logic and structure.

Here’s a snippet from the newly introduced home.test.tsx file:

import { render, screen } from '@testing-library/react';
import Home from '../Home';
import { HelmetProvider } from 'react-helmet-async';

test('renders Home component correctly', () => {
  render(
    <HelmetProvider>
      <Home />
    </HelmetProvider>
  );
  
  const headerElement = screen.getByText(/welcome to home page/i);
  expect(headerElement).toBeInTheDocument();
});

Configuration Updates

The vitest.config.ts file was updated to remove references to the deleted test files, ensuring a cleaner and more maintainable configuration.

Context Providers and Selector Improvements

To ensure seamless integration without mocks, we wrapped certain components with context providers. For instance, the HelmetProvider was added to the Home tests to prevent errors related to react-helmet-async. Additionally, we replaced generic role selectors with specific data-testid selectors to prevent test failures due to ambiguous element matches.

Here’s an example from the updated home.test.tsx:

test('renders with specific data-testid', () => {
  render(<Home />);
  const specificElement = screen.getByTestId('home-header');
  expect(specificElement).toBeInTheDocument();
});

Simplification and Reduction

The layout tests were a prime example of simplification, where complex mocked tests were reduced significantly, adhering to the principle that complex components with dependencies are more suited for end-to-end (E2E) testing.

Key Code Changes

  • Removal of Mock Dependencies: Transitioned from complex mocks to using context providers and specific selectors.
  • Test Simplification: Reduced the lines of code in tests, particularly in the layout tests, from 274 to just 19 lines.
  • Configuration Clean-Up: Updated the vitest configuration to reflect the new testing structure.

Impact and Benefits

The shift to bulletproof testing without mocks has resulted in several tangible benefits:

  • Increased Reliability: Tests are now more consistent and less prone to hanging or failing due to external dependencies.
  • Enhanced Speed: Isolated tests execute faster, contributing to a more efficient development workflow.
  • Improved Maintainability: With simplified tests and clear configurations, the codebase is easier to manage and extend.
  • Broader Coverage: Comprehensive tests for key components ensure that the UI logic and structure are thoroughly validated.
  • AI-Friendly Guidelines: The Bulletproof Testing Guidelines now serve as guardrails for AI agents, preventing them from creating overly complex test scenarios.

Lessons Learned

This experience taught me an important lesson about working with AI coding assistants: while they can generate comprehensive tests, they often default to complex solutions that mirror patterns from their training data. The AI agent’s tendency to create elaborate mock scenarios reflected common but problematic testing practices found across many codebases.

By establishing clear guidelines and principles for testing, we can guide both human developers and AI assistants toward simpler, more maintainable solutions. The key is to be explicit about what we want: fast, isolated, reliable tests that focus on actual behavior rather than implementation details.

In conclusion, the transition to bulletproof tests marks a significant improvement in our testing strategy. By eliminating dependency on mocks and focusing on test isolation, we’ve achieved a more reliable and maintainable testing environment. This approach not only enhances our current development practices but also sets a strong foundation for future growth - whether that growth comes from human developers or AI assistants.


By adopting these principles, developers can ensure their test suites are robust, reliable, and maintainable, ultimately leading to higher quality software and a smoother development process.

Discuss on Bluesky

Recent replies

drk

Are you testing any components that have complex data dependencies/using something like TanStack query? I am currently adding MSW for mocking network, but asserting UI correctness, but still requires complex factories/stubbing for things like current user/org etc.

View on Bluesky →