Testovací pyramida

Testovací pyramida popisuje doporučený poměr různých typů testů. Čím níže v pyramidě, tím rychlejší, levnější a více testů. Čím výše, tím realističtější ale pomalejší.

E2E testy
Celá aplikace v reálném prohlížeči — Playwright, Cypress
~10%
sekund–minuty
Integration testy
Více modulů dohromady — API endpointy, DB, komponenty
~20%
ms–sekundy
Unit testy
Izolovaná funkce nebo komponenta — Vitest, Jest
~70%
ms
TypCo testujeNástrojeRychlost
UnitIzolovaná funkce, hook, utilitaVitest, Jest⚡ Velmi rychlé
IntegrationKomponenta + store, API + DBVitest + MSW, Supertest🏃 Rychlé
E2ECelý uživatelský tok v prohlížečiPlaywright, Cypress🐢 Pomalé
🧪
TDD (Test-Driven Development): Napište test před implementací. Red → Green → Refactor. Nutí vás přemýšlet o API před kódem. Není vždy praktické, ale u utility funkcí a algoritmů se velmi vyplatí.
// 01 / 09 Vitest setup →

Vitest — moderní test runner

Vitest je Vite-nativní test runner — stejná konfigurace jako aplikace, ESM podpora, velmi rychlý hot reload testů. Pro nové projekty preferujte Vitest před Jest.

bash — Vitest setup
# Instalace (Vite projekt) $ npm install -D vitest @vitest/ui jsdom $ npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom # Spuštění $ npx vitest # watch mode $ npx vitest run # jednorázový run (CI) $ npx vitest --ui # vizuální UI $ npx vitest --coverage # coverage report # Výstup PASS src/utils/format.test.ts (3 tests) PASS src/components/Button.test.tsx (5 tests) FAIL src/hooks/useCart.test.ts (1 failed)
// vite.config.ts — přidat test konfigurace
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',     // simuluje browser DOM
    globals:     true,         // describe, it, expect bez importu
    setupFiles:  ['./src/test/setup.ts'],
    coverage: {
      reporter: ['text', 'lcov', 'html'],
      exclude:  ['node_modules/', 'src/test/'],
    },
  },
});

// src/test/setup.ts
import '@testing-library/jest-dom'; // přidá custom matchers
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

afterEach(() => {
  cleanup(); // vyčistí DOM po každém testu
});
// 02 / 09 Unit testy →

Unit testy — funkce a utility

Unit test testuje jednu funkci v izolaci. Vždy stejný vstup → stejný výstup. Žádné side effects, žádné závislosti (mockovat je).

// src/utils/format.ts
export function formatCena(castka: number, mena = 'Kč'): string {
  if (castka < 0) throw new Error('Cena nemůže být záporná');
  return `${castka.toLocaleString('cs-CZ')} ${mena}`;
}

export function slugify(text: string): string {
  return text
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '') // remove diacritics
    .replace(/[^a-z0-9\s-]/g, '')
    .trim()
    .replace(/\s+/g, '-');
}

// src/utils/format.test.ts
import { describe, it, expect } from 'vitest';
import { formatCena, slugify } from './format';

describe('formatCena', () => {
  it('formátuje číslo s měnou', () => {
    expect(formatCena(299)).toBe('299 Kč');
    expect(formatCena(1000)).toBe('1 000 Kč');
  });

  it('akceptuje vlastní měnu', () => {
    expect(formatCena(100, 'EUR')).toBe('100 EUR');
  });

  it('vyhodí chybu pro zápornou cenu', () => {
    expect(() => formatCena(-1)).toThrow('Cena nemůže být záporná');
  });
});

describe('slugify', () => {
  it('převede text na slug', () => {
    expect(slugify('Kapesní nůž')).toBe('kapesni-nuz');
    expect(slugify('Hello World!')).toBe('hello-world');
  });

  it('odstraní speciální znaky', () => {
    expect(slugify('  Více  mezer  ')).toBe('vice-mezer');
  });
});

// Matchers — nejpoužívanější:
// expect(val).toBe(2)              // přesná rovnost (===)
// expect(val).toEqual({ a: 1 })    // hluboká rovnost (deep equal)
// expect(val).toBeTruthy()         // truthy hodnota
// expect(val).toBeNull()           // null
// expect(arr).toHaveLength(3)      // délka pole
// expect(arr).toContain('item')    // obsahuje prvek
// expect(str).toMatch(/regex/)     // match regex
// expect(fn).toThrow('msg')        // vyhodí chybu
// expect(spy).toHaveBeenCalledWith(arg) // volání spy

// Kvíz: Co je správná struktura pro pojmenování unit testu?

React Testing Library

RTL testuje komponenty z pohledu uživatele — ne implementaci, ale chování. Principy: dotazovat prvky jako uživatel (role, label, text), ne jako developer (class, id, data-testid).

import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
import { LoginForm } from './LoginForm';

// ── Základní render + assertions ───────────────────────────
describe('Button', () => {
  it('renderuje text', () => {
    render(<Button label="Odeslat" />);
    expect(screen.getByText('Odeslat')).toBeInTheDocument();
  });

  it('volá onClick při kliknutí', async () => {
    const handleClick = vi.fn(); // mock funkce
    render(<Button label="Klikni" onClick={handleClick} />);

    await userEvent.click(screen.getByRole('button', { name: 'Klikni' }));
    expect(handleClick).toHaveBeenCalledOnce();
  });

  it('je disabled když prop disabled=true', () => {
    render(<Button label="Neklíkej" disabled />);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

// ── Složitější komponenta s formulářem ─────────────────────
describe('LoginForm', () => {
  it('zobrazí chybu pro prázdný email', async () => {
    render(<LoginForm onSubmit={vi.fn()} />);

    await userEvent.click(screen.getByRole('button', { name: /přihlásit/i }));
    expect(await screen.findByText(/email je povinný/i)).toBeInTheDocument();
  });

  it('zavolá onSubmit se správnými daty', async () => {
    const onSubmit = vi.fn();
    render(<LoginForm onSubmit={onSubmit} />);

    await userEvent.type(
      screen.getByLabelText(/email/i),
      'jana@example.com'
    );
    await userEvent.type(
      screen.getByLabelText(/heslo/i),
      'tajneheslo123'
    );
    await userEvent.click(screen.getByRole('button', { name: /přihlásit/i }));

    expect(onSubmit).toHaveBeenCalledWith({
      email:    'jana@example.com',
      password: 'tajneheslo123',
    });
  });
});

// Dotazovací priority (getBy > findBy > queryBy):
// getByRole('button')       ← přístupnost (preferred!)
// getByLabelText(/email/i)  ← formulář
// getByText(/odeslat/i)     ← viditelný text
// getByPlaceholderText()    ← fallback
// getByTestId('my-element') ← poslední záchrana
💡
getBy vs findBy vs queryBy: getBy — synchronní, vyhodí chybu pokud nenajde. findBy — asynchronní (čeká na async operaci), vrátí Promise. queryBy — synchronní, vrátí null pokud nenajde (pro asserting absence: expect(queryBy...).not.toBeInTheDocument()).
// 04 / 09 Mocking →

Mocking — izolace závislostí

Mock nahradí skutečnou závislost (API, databáze, modul) kontrolovanou alternativou. Testy běží rychle a deterministicky bez sítě nebo DB.

import { vi, describe, it, expect, beforeEach } from 'vitest';

// ── vi.fn() — mock funkce ───────────────────────────────
const mockFetch = vi.fn();
mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve([]) });

// Assertions na mock
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith('/api/products');
expect(mockFetch).toHaveBeenLastCalledWith(expect.stringContaining('/api'));

// ── vi.mock() — mockovat modul ──────────────────────────
vi.mock('../lib/prisma', () => ({
  prisma: {
    product: {
      findMany: vi.fn().mockResolvedValue([
        { id: 1, nazev: 'Test nůž', cena: 299 }
      ]),
      create: vi.fn().mockImplementation((data) =>
        Promise.resolve({ id: Math.random(), ...data.data })
      ),
    },
  },
}));

// ── MSW (Mock Service Worker) — mock HTTP API ───────────
// npm install -D msw
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  http.get('/api/products', () => {
    return HttpResponse.json([
      { id: 1, nazev: 'Victorinox', cena: 299 }
    ]);
  }),

  http.post('/api/products', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: 42, ...body }, { status: 201 });
  }),

  http.get('/api/products/:id', ({ params }) => {
    if (params.id === '999') {
      return HttpResponse.json({ error: 'Nenalezeno' }, { status: 404 });
    }
    return HttpResponse.json({ id: parseInt(params.id), nazev: 'Test' });
  }),
);

// Setup v testech
beforeAll(()  => server.listen());
afterEach(()  => server.resetHandlers());
afterAll(()   => server.close());

// Override pro konkrétní test
it('zpracuje 404 chybu', async () => {
  server.use(
    http.get('/api/products/1', () =>
      HttpResponse.json({ error: 'Nenalezeno' }, { status: 404 })
    )
  );
  // ... test logika
});

// Kvíz: Proč je MSW (Mock Service Worker) lepší než přímé mockování fetch pro React komponenty?

Integration testy — API a DB

Integration testy testují spolupráci více vrstev — Express handler + validace + databáze. Používají skutečnou (test) DB nebo in-memory alternativu.

// tests/api/products.integration.test.ts
// npm install -D supertest @types/supertest

import request  from 'supertest';
import app      from '../../src/app';
import { prisma } from '../../src/lib/prisma';
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';

describe('POST /api/products', () => {
  // Cleanup před každým testem
  beforeEach(async () => {
    await prisma.product.deleteMany();
  });

  afterAll(async () => {
    await prisma.$disconnect();
  });

  it('vytvoří nový produkt s platnými daty', async () => {
    const response = await request(app)
      .post('/api/products')
      .set('Authorization', `Bearer ${testToken}`)
      .send({ nazev: 'Test nůž', slug: 'test-nuz', cena: 299 });

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject({
      id:    expect.any(Number),
      nazev: 'Test nůž',
      cena:  '299', // Decimal → string v JSON
    });

    // Ověřit v DB
    const dbProduct = await prisma.product.findUnique({
      where: { id: response.body.id }
    });
    expect(dbProduct).not.toBeNull();
  });

  it('vrátí 400 pro chybějící povinná pole', async () => {
    const response = await request(app)
      .post('/api/products')
      .set('Authorization', `Bearer ${testToken}`)
      .send({ nazev: 'Bez ceny' }); // chybí cena

    expect(response.status).toBe(400);
    expect(response.body.error).toMatch(/validační/i);
  });

  it('vrátí 401 bez JWT tokenu', async () => {
    const response = await request(app)
      .post('/api/products')
      .send({ nazev: 'Test', cena: 299 });

    expect(response.status).toBe(401);
  });
});

// Component integration test — s Context
describe('ProductList + CartContext', () => {
  it('přidá produkt do košíku při kliknutí', async () => {
    render(
      <CartProvider>
        <ProductList products={mockProducts} />
        <CartSummary />
      </CartProvider>
    );

    await userEvent.click(screen.getAllByText('Do košíku')[0]);
    expect(screen.getByText(/1 položka/i)).toBeInTheDocument();
  });
});
// 06 / 09 Playwright →

Playwright — E2E testy

Playwright automatizuje skutečný prohlížeč (Chromium, Firefox, WebKit). E2E testy testují celý uživatelský tok — od kliknutí po DB zápis.

bash — Playwright setup
$ npm create playwright@latest $ npx playwright test # run all $ npx playwright test --ui # vizuální UI $ npx playwright test --headed # viditelný prohlížeč $ npx playwright codegen localhost:3000# nahrávač akcí
// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Autentizace', () => {
  test('přihlášení a přesměrování na dashboard', async ({ page }) => {
    await page.goto('/login');

    // Vyplnit formulář
    await page.getByLabel('Email').fill('admin@example.com');
    await page.getByLabel('Heslo').fill('adminpass123');
    await page.getByRole('button', { name: /přihlásit/i }).click();

    // Počkat na navigaci
    await page.waitForURL('/dashboard');
    await expect(page.getByText('Vítej, Admin!')).toBeVisible();

    // Ověřit, že token je uložen
    const token = await page.evaluate(() => localStorage.getItem('token'));
    expect(token).toBeTruthy();
  });

  test('zobrazí chybu pro špatné heslo', async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('user@example.com');
    await page.getByLabel('Heslo').fill('spatneheslo');
    await page.getByRole('button', { name: /přihlásit/i }).click();

    await expect(page.getByText(/neplatné přihlašovací údaje/i)).toBeVisible();
    expect(page.url()).toContain('/login'); // zůstal na login
  });
});

// tests/e2e/checkout.spec.ts
test('kompletní checkout flow', async ({ page }) => {
  // Setup: přihlásit se
  await loginAsUser(page);

  // Přidat do košíku
  await page.goto('/produkty');
  await page.getByText('Victorinox Classic').first().click();
  await page.getByRole('button', { name: /do košíku/i }).click();

  // Ověřit badge košíku
  await expect(page.getByTestId('cart-badge')).toHaveText('1');

  // Checkout
  await page.goto('/checkout');
  await page.getByLabel('Jméno').fill('Jana Nováková');
  await page.getByLabel('Adresa').fill('Wenceslas Square 1, Praha');
  await page.getByRole('button', { name: /objednat/i }).click();

  // Potvrzení
  await expect(page.getByText(/děkujeme za objednávku/i)).toBeVisible();
});

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  reporter: 'html',
  use: {
    baseURL:     'http://localhost:3000',
    screenshot:  'only-on-failure',
    video:       'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'mobile',   use: { ...devices['iPhone 14'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url:     'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

// Kvíz: Proč by se E2E testy neměly spoléhat na CSS třídy nebo data-testid pro lokalizaci prvků?

// 07 / 09 Cvičení →

Cvičení — Napište testy pro shopping cart

Napište unit testy pro cartReducer a integration test pro komponentu CartItem s RTL.

// src/reducers/cartReducer.ts (to je testovaný kód)
export function cartReducer(state: CartItem[], action: CartAction): CartItem[] {
  switch (action.type) {
    case 'ADD': {
      const exists = state.find(i => i.id === action.item.id);
      if (exists) {
        return state.map(i =>
          i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i
        );
      }
      return [...state, { ...action.item, qty: 1 }];
    }
    case 'REMOVE':
      return state.filter(i => i.id !== action.id);
    case 'CLEAR':
      return [];
    default:
      return state;
  }
}

// src/reducers/cartReducer.test.ts — NAPIŠTE TESTY:
describe('cartReducer', () => {
  it('přidá novou položku do prázdného košíku', () => {
    // TODO: zavolat cartReducer s [] a ADD akcí
    // expect výsledek délky 1 a qty 1
  });

  it('zvýší qty pro existující položku', () => {
    // TODO: stav s existující položkou + ADD stejné ID
    // expect qty = 2
  });

  it('odebere položku přes REMOVE', () => {
    // TODO
  });

  it('vymaže košík přes CLEAR', () => {
    // TODO
  });
});

// CartItem.test.tsx — NAPIŠTE RTL TEST:
describe('CartItem', () => {
  it('zobrazí název a cenu produktu', () => {
    // TODO: render <CartItem> s mock daty
  });

  it('zavolá onRemove při kliknutí na X', async () => {
    // TODO: render, userEvent.click, expect mock fn
  });
});
🏅

Quality Engineer

Zvládáte unit testy, RTL, mocking s MSW a Playwright E2E.

// 08 / 09 Taháček →

// Taháček

// Vitest základní struktura
describe('Komponenta', () => {
  beforeEach(() => { /* setup */ });
  afterEach(()  => { cleanup(); });

  it('dělá X když Y', () => {
    expect(result).toBe(expected);
    expect(obj).toEqual({ a: 1 });
    expect(fn).toThrow('message');
    expect(mock).toHaveBeenCalledWith(arg);
  });
});

// RTL dotazy (priority order)
getByRole('button', { name: /text/i })
getByLabelText(/email/i)
getByText(/odeslat/i)
findByText(/async text/i) // returns Promise

// Mocking
const mock = vi.fn().mockReturnValue(42);
vi.mock('./module', () => ({ fn: vi.fn() }));

// Playwright
await page.goto('/url');
await page.getByRole('button').click();
await page.getByLabel('Email').fill('text');
await expect(page.getByText('Hello')).toBeVisible();
await page.waitForURL('/dashboard');
// Lekce 7 dokončena L8: WebSockets & Realtime →