If you've been putting off learning unit testing because it feels like a rabbit hole, here's the good news: you only need a handful of concepts to get productive. This post covers config, coverage, thresholds, mocks, and a few real examples — nothing more.
1. Setting Up: Vitest vs Jest
If you're on Vite, use Vitest — it reuses your Vite config and is faster. If you're on CRA/Webpack, use Jest.
Vitest config (vite.config.ts)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.ts',
},
})
Jest config (jest.config.js)
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEach: ['<rootDir>/src/setupTests.js'],
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy',
},
}
Key options in both:
environment: 'jsdom'— simulates a browser DOM so you can render components.setupFiles— loads testing-library matchers (e.g.@testing-library/jest-dom) before every test file.
2. Coverage
Coverage tells you how much of your code ran during tests — not whether the tests are good, just how much was touched.
Enable coverage (Vitest)
vitest run --coverage
// vite.config.tstest: {
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'html'],
},
}
Enable coverage (Jest)
jest --coverage
// jest.config.jscollectCoverage: true,
coverageReporters: ['text', 'html'],
Running this gives you a report like:
File | % Stmts | % Branch | % Funcs | % Lines
--------------|---------|----------|---------|--------
Button.tsx | 90.9 | 75 | 100 | 90.9
Four metrics matter:
- Statements — individual lines executed
- Branches — if/else, ternary paths executed
- Functions — functions called at least once
- Lines — similar to statements, line-based
3. Coverage Thresholds
Thresholds let you fail the build if coverage drops below a set percentage — useful in CI to stop coverage from silently eroding.
Vitest
coverage: {
thresholds: {
lines: 80,
functions: 80,
branches: 70,
statements: 80,
},
}
Jest
coverageThreshold: {
global: {
lines: 80,
functions: 80,
branches: 70,
statements: 80,
},
}
Practical tip: Start low (50–60%) on an existing project and raise it gradually. Setting 90% on day one just gets the check disabled by a frustrated teammate later.
4. Mocks
Mocks replace real dependencies (API calls, modules, timers) with fake, controllable versions — so tests are fast and don't depend on a real backend.
Mocking a function
const mockFn = vi.fn() // Vitest
// const mockFn = jest.fn() // Jest
mockFn('hello')
expect(mockFn).toHaveBeenCalledWith('hello')
Mocking a module (e.g. an API service)
vi.mock('../services/userService', () => ({
getUser: vi.fn(() => Promise.resolve({ id: 1, name: 'Fayaz' })),
}))
Now any component importing getUser gets the fake version — no real network call happens.
Mocking fetch/axios in a component test
import { render, screen, waitFor } from '@testing-library/react'
import UserCard from './UserCard'
import { getUser } from '../services/userService'
vi.mock('../services/userService')
test('renders user name after fetch', async () => {
getUser.mockResolvedValue({ id: 1, name: 'Fayaz' })
render(<UserCard userId={1} />)
await waitFor(() => {
expect(screen.getByText('Fayaz')).toBeInTheDocument()
})
})
5. Simple Examples
Testing a pure function
// utils/formatCurrency.ts
export function formatCurrency(amount: number) {
return `₹${amount.toFixed(2)}`
}
// utils/formatCurrency.test.ts
import { formatCurrency } from './formatCurrency'
test('formats number as currency', () => {
expect(formatCurrency(100)).toBe('₹100.00')
})
Testing a component render
// Button.tsx
export function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>
}
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
test('calls onClick when clicked', () => {
const handleClick = vi.fn()
render(<Button label="Save" onClick={handleClick} />)
fireEvent.click(screen.getByText('Save'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
Testing a service (business logic layer)
// services/orderService.ts
export function calculateTotal(items: { price: number; qty: number }[]) {
return items.reduce((sum, item) => sum + item.price * item.qty, 0)
}
// services/orderService.test.ts
import { calculateTotal } from './orderService'
test('calculates total price correctly', () => {
const items = [{ price: 100, qty: 2 }, { price: 50, qty: 1 }]
expect(calculateTotal(items)).toBe(250)
})
Notice the pattern: components are tested by rendering + interacting, services/utils are tested by calling the function directly with inputs and checking outputs. That split makes tests easier to write and faster to run — one more reason to keep business logic out of components.
Takeaway
You don't need to memorize every Jest/Vitest API to start testing. Pick the runner that matches your build tool, turn on coverage with a modest threshold, mock external dependencies, and write small, focused tests for functions and components separately. That's 90% of what you'll use day-to-day.