React|Architecture and Design

Testing by Layer with Vitest: What to Test and What to Give Up

7

Chris gets nervous whenever deployment day arrives.

"Does the login work? Can I add items to the cart? Did anyone check the payment button?"

He manually clicks through every feature to verify them. But as the number of features surpassed 100, this manual testing became an impossible mission.

"I need to write test code!"

He made a resolution and started writing tests. But as he tried to test every component, every style, and every button click, the cost of maintenance began to outweigh the benefits. Fixing one feature caused 10 tests to break.

Testing is about ROI (Return on Investment). You cannot test everything.

Today, using Vitest, we will establish a strategy based on the 3-Layer Architecture (Domain, Data, Presentation) we discussed previously, distinguishing between what must be tested and what should be boldly abandoned.Getty Images

1. Testing Principles: What Should We Test?

Kent C. Dodds, a master of frontend testing, said this:

"The more your tests resemble the way your software is used, the more confidence they can give you."
  • ❌ Implementation Details: Checking if a div tag has class="active", or if an internal state variable is named isOpen. (These break immediately upon refactoring.)
  • βœ… User Behavior: "Does the 'Success' message appear when the button is pressed?" (If the result is the same even after code changes, the test passes.)
  • We will apply this principle to our 3-Layer Architecture.

    2. Layer 1: Domain Layer (Best ROI πŸ’Ž)

    This is where Business Logic (Custom Hooks, Stores, Utilities) resides.

    Since only JavaScript logic exists here without any UI, testing speed is incredibly fast, and writing tests is easy. This is the area you must test most thoroughly.

    What to test?

  • State changes in Hooks (useCounter, useCart).
  • Complex calculation logic (calculateTotal).
  • typescript
    // tests/useCounter.test.ts
    import { renderHook, act } from '@testing-library/react';
    import { describe, it, expect } from 'vitest';
    import { useCounter } from '../model/useCounter';
    
    describe('useCounter', () => {
      it('Initial value should be 0', () => {
        const { result } = renderHook(() => useCounter());
        expect(result.current.count).toBe(0);
      });
    
      it('Value should increase by 1 when increment is called', () => {
        const { result } = renderHook(() => useCounter());
        
        // Functions that update state must be wrapped in act
        act(() => {
          result.current.increment();
        });
    
        expect(result.current.count).toBe(1);
      });
    });

    3. Layer 2: Data Layer (Building a Shield πŸ›‘οΈ)

    Here, we test the Mapper and DTO handling that transforms server data structures.

    Do not call actual APIs (they are slow and unstable). Instead, use Mocking or test only the pure function Mappers.

    What to test?

  • Mapper Functions: Whether they handle unexpected values (null, undefined) from the server correctly with defaults.
  • What to give up?

  • Whether axios.get actually sends a request. (That is the Axios library's job, not yours.)
  • typescript
    // tests/userMapper.test.ts
    import { userMapper } from '../lib/userMapper';
    import { describe, it, expect } from 'vitest';
    
    describe('userMapper', () => {
      it('Should use the default image if profile_img from server is null', () => {
        // 1. Mock Data
        const mockDTO = {
          user_id: 1,
          user_name: 'Chris',
          profile_img: null, // Null scenario
        };
    
        // 2. Execution
        const result = userMapper(mockDTO);
    
        // 3. Verification
        expect(result.profileUrl).toBe('https://default-image.com');
      });
    });

    4. Layer 3: Presentation Layer (Target Only Needs 🎯)

    This involves Component Rendering tests. These are the most fragile and have the highest maintenance costs.

    Here, we focus on Integration Tests centered around "User Scenarios."

    What to test?

  • Critical User Flows: Submitting a login form, Opening/Closing a modal.
  • Conditional Rendering: Does the spinner appear while loading? Does the error message appear on error?
  • What to give up? (Important!)

  • Simple UI Placement: "Is the button 20px from the right?" (Check CSS with your eyes).
  • Detailed DOM Structure: "Is the list a ul > li structure or a div structure?" (Users don't care about tags).
  • typescript
    // tests/UserProfile.test.tsx
    import { render, screen, waitFor } from '@testing-library/react';
    import { UserProfile } from '../ui/UserProfile';
    import { vi, describe, it, expect } from 'vitest';
    
    // Mock the hook to separate Logic from UI
    vi.mock('../model/useUser', () => ({
      useUser: () => ({
        user: { name: 'Chris' },
        isLoading: false,
      }),
    }));
    
    describe('UserProfile', () => {
      it('Should display the user name if data exists', () => {
        render(<UserProfile userId={1} />);
        
        // Check if the text "Chris" is in the document
        expect(screen.getByText('Chris')).toBeInTheDocument();
      });
    });

    5. Mocking Strategy: MSW (Mock Service Worker)

    Intercepting axios.get one by one with jest.fn() when testing API calls is tedious.

    MSW intercepts requests at the network level and returns fake responses. It allows you to test as if a real server exists.

    typescript
    // handlers.ts (MSW Setup)
    import { http, HttpResponse } from 'msw';
    
    export const handlers = [
      http.get('/api/users/1', () => {
        return HttpResponse.json({ user_name: 'Mock Chris' });
      }),
    ];

    By setting this up, you can test the entire flow in integration tests (like App.test.tsx) without poking the real API.

    Key Takeaways

    Testing is done to gain confidence. Do not obsess over 100% coverage; defend the most fragile and critical parts first.

  • Domain Layer (Hooks/Utils): Priority 1. Logic is complex, so unit test thoroughly. (vitest + renderHook)
  • Data Layer (Mapper): Priority 2. Test if data transformation logic is safe. (Pure function tests)
  • Presentation Layer (Components): Priority 3. Integration test only core user scenarios (Login, Payment). Give up on trivial UI details. (testing-library)

  • We have built the architecture and even finished testing. Now it's time to release this amazing app to the world.

    Let's build a CI/CD pipeline, an automated deployment system that eliminates the excuse, "It works on my machine."

    The final journey of the roadmap continues in: "Building a CI/CD Pipeline with Vercel and Managing Environment Variables."

    πŸ”— References

  • Vitest Getting Started
  • React Testing Library
  • Testing Implementation Details (Kent C. Dodds)
  • Mock Service Worker (MSW)
  • Comments (0)

    0/1000 characters
    Loading comments...