Skip to main content

๐Ÿงฑ Part 13 — Testing Strategy (Vitest + Testing Library + MSW) ๐Ÿงช


A great test setup is fast, reliable, and mirrors how users actually use your app. We’ll set up Vitest (fast runner), React Testing Library (test behavior, not internals), and MSW (mock server) — with clear, commented examples for components, hooks, and Redux slices, plus API integration tests.

Philosophy:

  • Test the public surface (what users see/do), not implementation details.
  • Prefer queries by role/label/text over test IDs.
  • Keep unit tests small; add a few integration tests where flows matter.

1) Install test tooling

pnpm add -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
pnpm add -D msw whatwg-fetch            # MSW for API mocks, fetch polyfill for jsdom

2) Configure Vitest (Vite + jsdom + setup file)

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',        // DOM-like env for component tests
    globals: true,               // expect(), vi available globally
    setupFiles: ['./src/test/setupTests.ts'],
    css: true,                   // allow importing CSS in tests
    coverage: {
      reporter: ['text', 'lcov'],
      include: ['src/**/*.{ts,tsx}'],
      exclude: ['src/**/stories.*', 'src/main.tsx'],
    },
  },
});

Add scripts:

// package.json
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:cov": "vitest run --coverage"
  }
}

3) Global test setup (RTL + jest-dom + MSW)

// src/test/setupTests.ts
import '@testing-library/jest-dom/vitest';
import { afterAll, afterEach, beforeAll } from 'vitest';
import { server } from './testServer';

// Start MSW once for all tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));

// Reset handlers between tests (avoid cross-test leakage)
afterEach(() => server.resetHandlers());

// Cleanup after the test suite
afterAll(() => server.close());

Define the MSW server and handlers:

// src/test/testServer.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

export const handlers = [
  // Example API: GET /tasks
  http.get('http://localhost:3000/tasks', () => {
    return HttpResponse.json({
      items: [
        { id: '1', title: 'Buy milk', done: false, created_at: new Date().toISOString() },
      ],
      total: 1,
    });
  }),
  // Toggle
  http.patch('http://localhost:3000/tasks/:id/toggle', ({ params }) => {
    return HttpResponse.json({ id: params.id, title: 'Buy milk', done: true, created_at: new Date().toISOString() });
  }),
];

export const server = setupServer(...handlers);

Adjust the base URL to your VITE_API_URL during tests or ensure your axios instance points to http://localhost:3000 in test env.


4) Test a UI primitive (Button)

// src/components/ui/__tests__/Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../Button';

it('renders with text and handles clicks', async () => {
  const user = userEvent.setup();
  const onClick = vi.fn();
  render(<Button onClick={onClick}>Save</Button>);

  // Query by role is preferred for buttons
  const btn = await screen.findByRole('button', { name: /save/i });
  await user.click(btn);
  expect(onClick).toHaveBeenCalledTimes(1);
});

it('shows loading state and disables interaction', async () => {
  render(<Button isLoading>Submit</Button>);
  const btn = await screen.findByRole('button', { name: /⏳/ });
  expect(btn).toBeDisabled();
  expect(btn).toHaveAttribute('aria-busy', 'true');
});

5) Test a form with validation (Input + React Hook Form + Zod)

// src/features/forms/__tests__/TaskFormView.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TaskFormView } from '@/features/forms/TaskForm';

it('validates title and submits data', async () => {
  const user = userEvent.setup();
  const submit = vi.fn();
  render(<TaskFormView onSubmit={submit} />);

  // Submit without typing → expect validation error
  await user.click(screen.getByRole('button', { name: /save task/i }));
  expect(await screen.findByText(/at least 3 characters/i)).toBeInTheDocument();

  await user.type(screen.getByLabelText(/title/i), 'Write tests');
  await user.click(screen.getByRole('button', { name: /save task/i }));
  expect(submit).toHaveBeenCalledWith(
    expect.objectContaining({ title: 'Write tests' }),
  );
});

6) Test a data-fetching component with MSW (Tasks page)

// src/pages/tasks/__tests__/Tasks.test.tsx
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import TasksPage from '@/pages/tasks/Tasks';

function renderWithQuery(ui: React.ReactNode) {
  const qc = new QueryClient();
  return render(<QueryClientProvider client={qc}>{ui}</QueryClientProvider>);
}

it('renders tasks from API and toggles', async () => {
  renderWithQuery(<TasksPage />);

  // Wait for list
  const item = await screen.findByText(/buy milk/i);
  expect(item).toBeInTheDocument();

  // Toggle checkbox (optimistic update + server OK)
  const checkbox = screen.getByRole('checkbox');
  expect(checkbox).not.toBeChecked();
  await (await import('@testing-library/user-event')).default.setup().then(u => u.click(checkbox));
  // After mutation it should be checked
  expect(checkbox).toBeChecked();
});

7) Test a custom hook (Timer logic example)

// src/hooks/__tests__/useToggle.test.tsx
import { renderHook, act } from '@testing-library/react';
import { useToggle } from '@/hooks/useToggle';

it('toggles boolean value', () => {
  const { result } = renderHook(() => useToggle(false));
  expect(result.current.value).toBe(false);
  act(() => result.current.toggle());
  expect(result.current.value).toBe(true);
});

@testing-library/react re-exports a renderHook helper when using @testing-library/react-hooks is not available. If needed, install @testing-library/react-hooks or convert the hook usage to a tiny test component.


8) Test a Redux slice (pure reducer)

// src/store/__tests__/uiSlice.test.ts
import reducer, { toggleTheme, setModal, setFilter } from '@/store/uiSlice';

it('toggles theme', () => {
  const state = reducer(undefined, { type: 'init' } as any);
  const next = reducer(state, toggleTheme());
  expect(next.theme).not.toBe(state.theme);
});

it('opens modal and sets filter', () => {
  const state = reducer(undefined, { type: 'init' } as any);
  const opened = reducer(state, setModal(true));
  expect(opened.modalOpen).toBe(true);
  const filtered = reducer(opened, setFilter({ search: 'react', showDone: false }));
  expect(filtered.filter).toEqual({ search: 'react', showDone: false });
});

9) Mocking error cases with MSW

// src/test/testServer.error.ts (optional alternative handler)
import { http, HttpResponse } from 'msw';

export const errorHandlers = [
  http.get('http://localhost:3000/tasks', () => HttpResponse.json({ message: 'Boom' }, { status: 500 })),
];

Swap in a test using server.use(...errorHandlers) to simulate failures and assert error UI.


10) Useful tips

  • Avoid implementation details: don’t test internal state; test the UI reactions.
  • Act like a user: use userEvent (click, type) instead of firing raw events.
  • Stable queries: prefer getByRole/getByLabelText/findByText over test IDs.
  • Keep tests deterministic: mock dates/IDs if needed.
  • Coverage: use pnpm test:cov to track; aim for meaningful coverage, not 100%.

11) Optional CI (GitHub Actions)

# .github/workflows/test.yml
name: test
on: [push, pull_request]
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'pnpm' }
      - run: pnpm install --frozen-lockfile
      - run: pnpm test -- --run

✅ Wrap‑Up

You now have a fast, ergonomic test stack:

  • Vitest runner + jsdom
  • React Testing Library for behavior-first tests
  • MSW for reliable API mocking
  • Examples covering components, forms, hooks, Redux slices, and React Query flows

This baseline keeps your app stable as it grows while staying beginner‑friendly and maintainable.


Comments

Popular posts from this blog

๐ŸŒŸ Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. ๐Ÿงฉ A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

๐ŸŒ Why Do We Need Generics? Imagine you want to create a stack (like a pile of books ๐Ÿ“š): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } ๐Ÿ‘‰ Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. ๐Ÿ˜ข ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

๐Ÿšช Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data ๐Ÿ‘‰ Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...