Testing by Layer with Vitest: What to Test and What to Give Up
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."
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?
// 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?
What to give up?
// 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?
What to give up? (Important!)
// 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.
// 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.
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."