Przejdź do głównej zawartości

Generowanie i wykonywanie testów

Testowanie to sieć bezpieczeństwa, która pozwala refaktoryzować bez strachu, wdrażać z pewnością i spać spokojnie. Jednak większość developerów traktuje to jako dodatek - żmudny boilerplate napisany niechętnie po “prawdziwej” pracy. Claude Code odwraca ten scenariusz, czyniąc tworzenie testów tak naturalnym jak opisywanie tego, co Twój kod powinien robić.

Scenariusz: Właśnie skończyłeś implementację złożonego modułu płatności. Obsługuje wiele walut, nalicza rabaty, waliduje karty kredytowe i integruje się z trzema dostawcami płatności. Teraz potrzebujesz kompleksowych testów. Tradycyjne podejście? Godziny pisania przypadków testowych, mockowania zależności i łapania przypadków granicznych, które zapomniałeś. Z Claude Code? Zobacz sam.

// 1. Napisz test (ręcznie pomyśl o przypadkach)
test('should apply 10% discount', () => {
// Spędź czas na konfiguracji mocków
// Napisz asercję
// Uruchom test - nie przechodzi
// Zaimplementuj funkcję
// Uruchom test - miejmy nadzieję przechodzi
});
// Powtarzaj dla dziesiątek przypadków testowych...
// Przegap przypadki graniczne o których nie pomyślałeś
  1. Utwórz komendy testowe Zapisz w .claude/commands/test-driven.md:

    Podążaj ścisłym workflow TDD dla: $ARGUMENTS
    1. Napisz nieprzechodzące testy najpierw
    2. Uruchom testy aby potwierdzić że nie przechodzą
    3. Zaimplementuj minimalny kod aby przeszły
    4. Refaktoryzuj zachowując testy zielone
    5. Dodaj przypadki graniczne i scenariusze błędów
    Użyj naszego stosu testowego:
    - Jest do testów jednostkowych
    - React Testing Library do komponentów
    - Supertest do endpointów API
    - Playwright do testów E2E
  2. Skonfiguruj konfigurację testów

    > Utwórz kompleksową konfigurację testową dla naszego projektu Node.js.
    > Uwzględnij konfigurację Jest, helpery testowe, narzędzia mock i integrację CI
  3. Skonfiguruj integrację IDE

    Okno terminala
    # Włącz obserwowanie testów
    claude "Skonfiguruj Jest w trybie watch z raportowaniem pokrycia"
  4. Zainstaluj serwery MCP testowe

    Okno terminala
    # Do testowania przeglądarki
    claude mcp add playwright
    # Do testowania API
    claude mcp add postman

Przekształć wymagania bezpośrednio w przypadki testowe:

> Potrzebujemy systemu uwierzytelniania użytkowników z tymi wymaganiami:
> - Login email/hasło
> - OAuth (Google, GitHub)
> - Zarządzanie sesjami z JWT
> - Reset hasła przez email
> - Blokada konta po 5 nieudanych próbach
> - Opcjonalne 2FA
>
> Wygeneruj kompleksowe testy dla wszystkich tych funkcji

Claude generuje:

Testy jednostkowe

auth.service.test.js
describe('AuthService', () => {
describe('login', () => {
it('should authenticate valid credentials');
it('should reject invalid password');
it('should reject non-existent user');
it('should increment failed attempts');
it('should lock account after 5 failures');
it('should handle case-insensitive emails');
});
describe('password reset', () => {
it('should generate secure reset token');
it('should expire tokens after 1 hour');
it('should invalidate token after use');
it('should prevent enumeration attacks');
});
});

Testy integracyjne

auth.integration.test.js
describe('Auth API', () => {
it('POST /login returns JWT for valid user');
it('POST /login rate limits requests');
it('GET /oauth/google redirects correctly');
it('POST /reset-password sends email');
it('validates JWT in protected routes');
it('refreshes tokens correctly');
});

Testy E2E

auth.e2e.test.js
describe('Auth Flow', () => {
it('user can complete login flow');
it('user can reset forgotten password');
it('OAuth login creates new account');
it('2FA flow works correctly');
it('session persists across pages');
it('logout clears all sessions');
});

Testy bezpieczeństwa

auth.security.test.js
describe('Security', () => {
it('prevents SQL injection in login');
it('prevents XSS in user inputs');
it('enforces password complexity');
it('prevents timing attacks');
it('masks user existence on login fail');
it('prevents session fixation');
});

Claude nie tylko pisze opisy testów - implementuje kompletne, uruchamialne testy:

// Przykład: Kompletna implementacja testu
describe('AuthService', () => {
let authService;
let mockUserRepo;
let mockEmailService;
let mockTokenGenerator;
beforeEach(() => {
// Claude konfiguruje wszystkie mocks
mockUserRepo = {
findByEmail: jest.fn(),
updateFailedAttempts: jest.fn(),
lockAccount: jest.fn()
};
mockEmailService = {
sendPasswordReset: jest.fn()
};
authService = new AuthService(
mockUserRepo,
mockEmailService,
mockTokenGenerator
);
});
describe('login', () => {
it('should authenticate valid credentials', async () => {
// Arrange
const credentials = {
email: 'user@example.com',
password: 'SecurePass123!'
};
const hashedPassword = await bcrypt.hash(credentials.password, 10);
const user = {
id: 1,
email: credentials.email,
password: hashedPassword,
failedAttempts: 0,
isLocked: false
};
mockUserRepo.findByEmail.mockResolvedValue(user);
// Act
const result = await authService.login(credentials);
// Assert
expect(result).toHaveProperty('token');
expect(result).toHaveProperty('refreshToken');
expect(result.user).not.toHaveProperty('password');
expect(mockUserRepo.updateFailedAttempts).toHaveBeenCalledWith(1, 0);
});
it('should lock account after 5 failed attempts', async () => {
// Claude implementuje kompletny test z przypadkami granicznymi
const user = {
id: 1,
failedAttempts: 4,
isLocked: false
};
mockUserRepo.findByEmail.mockResolvedValue(user);
await expect(authService.login({
email: 'user@example.com',
password: 'wrong'
})).rejects.toThrow('Account locked');
expect(mockUserRepo.lockAccount).toHaveBeenCalledWith(1);
expect(mockUserRepo.updateFailedAttempts).toHaveBeenCalledWith(1, 5);
});
});
});
  1. Uruchom nieprzechodzące testy

    > Uruchom testy auth. Wszystkie powinny nie przechodzić ponieważ
    > nic jeszcze nie zaimplementowaliśmy
  2. Zaimplementuj minimalny kod

    > Zaimplementuj tylko tyle kodu ile potrzeba aby pierwszy test przeszedł.
    > Nie dodawaj żadnej funkcjonalności nie wymaganej przez testy
  3. Iteruj do zielonego

    > Kontynuuj implementację aż wszystkie testy przejdą. Pokaż mi
    > wyniki testów po każdej zmianie
  4. Refaktoryzuj z pewnością

    > Teraz gdy testy przechodzą, refaktoryzuj implementację dla lepszej
    > struktury i wydajności. Ciągle uruchamiaj testy aby upewnić się że nic się nie zepsuło

Poza konkretnymi przykładami, testuj właściwości które powinny zawsze zachodzić:

> Wygeneruj testy oparte na właściwościach dla naszego algorytmu sortowania.
> Powinien utrzymywać te właściwości niezależnie od wejścia:
> - Długość wyjścia równa długości wejścia
> - Wyjście zawiera te same elementy co wejście
> - Każdy element jest <= następny element
> - Sortowanie dwukrotne daje ten sam wynik

Claude tworzy:

import fc from 'fast-check';
describe('Właściwości algorytmu sortowania', () => {
it('maintains array length', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = sortArray([...arr]);
expect(sorted.length).toBe(arr.length);
})
);
});
it('preserves all elements', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = sortArray([...arr]);
const sortedCopy = [...sorted].sort((a, b) => a - b);
const originalSorted = [...arr].sort((a, b) => a - b);
expect(sortedCopy).toEqual(originalSorted);
})
);
});
it('is idempotent', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const once = sortArray([...arr]);
const twice = sortArray([...once]);
expect(twice).toEqual(once);
})
);
});
});

Upewnij się że Twoje testy rzeczywiście łapią błędy:

> Użyj testowania mutacyjnego aby zweryfikować jakość naszego zestawu testów.
> Wprowadź małe błędy i potwierdź że testy je łapią

Claude implementuje:

// Oryginalna funkcja
function calculateDiscount(price, discountPercent) {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Invalid discount');
}
return price * (1 - discountPercent / 100);
}
// Claude generuje mutacje i weryfikuje czy testy je łapią
const mutations = [
{
name: 'Mutacja graniczna',
change: 'discountPercent > 100',
to: 'discountPercent >= 100'
},
{
name: 'Mutacja operacji',
change: '1 - discountPercent',
to: '1 + discountPercent'
},
{
name: 'Mutacja stałej',
change: '/ 100',
to: '/ 1000'
}
];
// Weryfikuje że każda mutacja powoduje niepowodzenie testów
mutations.forEach(mutation => {
console.log(`Testing mutation: ${mutation.name}`);
// Zastosuj mutację i uruchom testy
// Upewnij się że przynajmniej jeden test nie przechodzi
});

Dla mikroserwisów, upewnij się że kontrakty API są utrzymywane:

> Wygeneruj testy kontraktu między naszym Order Service a Payment Service.
> Order Service oczekuje odpowiedzi płatności w określonym formacie
// Test kontraktu konsumenta (Order Service)
describe('Payment Service Contract', () => {
it('should return expected payment response structure', async () => {
const payment = await paymentService.processPayment({
amount: 100,
currency: 'USD',
method: 'credit_card'
});
// Oczekiwania kontraktu
expect(payment).toMatchObject({
id: expect.any(String),
status: expect.stringMatching(/^(pending|completed|failed)$/),
amount: expect.any(Number),
currency: expect.any(String),
processedAt: expect.any(String),
provider: expect.objectContaining({
name: expect.any(String),
transactionId: expect.any(String)
})
});
});
});
// Test kontraktu dostawcy (Payment Service)
describe('Payment Service Provider Contract', () => {
it('should fulfill Order Service expectations', async () => {
const response = await request(app)
.post('/payments')
.send({ amount: 100, currency: 'USD', method: 'credit_card' });
// Weryfikuj że odpowiedź pasuje do oczekiwań konsumenta
expect(response.body).toMatchSnapshot('payment-response-contract');
});
});
> Napisz kompleksowe testy dla komponentu React koszyka zakupów który:
> - Wyświetla przedmioty z ilościami
> - Oblicza sumy z podatkiem
> - Obsługuje aktualizacje ilości
> - Usuwa przedmioty
> - Pokazuje stan pusty
> - Persystuje do localStorage

Claude generuje:

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ShoppingCart } from './ShoppingCart';
describe('Komponent ShoppingCart', () => {
const mockItems = [
{ id: 1, name: 'Widget', price: 10, quantity: 2 },
{ id: 2, name: 'Gadget', price: 20, quantity: 1 }
];
beforeEach(() => {
localStorage.clear();
});
it('renders items with correct quantities and prices', () => {
render(<ShoppingCart items={mockItems} taxRate={0.08} />);
expect(screen.getByText('Widget')).toBeInTheDocument();
expect(screen.getByText('$10.00 × 2')).toBeInTheDocument();
expect(screen.getByText('Gadget')).toBeInTheDocument();
expect(screen.getByText('$20.00 × 1')).toBeInTheDocument();
});
it('calculates total with tax correctly', () => {
render(<ShoppingCart items={mockItems} taxRate={0.08} />);
const subtotal = 10 * 2 + 20 * 1; // $40
const tax = subtotal * 0.08; // $3.20
const total = subtotal + tax; // $43.20
expect(screen.getByText('Subtotal: $40.00')).toBeInTheDocument();
expect(screen.getByText('Tax: $3.20')).toBeInTheDocument();
expect(screen.getByText('Total: $43.20')).toBeInTheDocument();
});
it('updates quantity when changed', async () => {
const onUpdate = jest.fn();
render(
<ShoppingCart
items={mockItems}
taxRate={0.08}
onUpdateQuantity={onUpdate}
/>
);
const quantityInput = screen.getByLabelText('Quantity for Widget');
await userEvent.clear(quantityInput);
await userEvent.type(quantityInput, '5');
expect(onUpdate).toHaveBeenCalledWith(1, 5);
});
it('persists cart to localStorage', () => {
render(<ShoppingCart items={mockItems} taxRate={0.08} />);
const savedCart = JSON.parse(localStorage.getItem('cart'));
expect(savedCart).toEqual(mockItems);
});
// Claude dodaje również testy dostępności
it('is keyboard navigable', async () => {
render(<ShoppingCart items={mockItems} taxRate={0.08} />);
const firstButton = screen.getByLabelText('Remove Widget from cart');
firstButton.focus();
expect(document.activeElement).toBe(firstButton);
// Tab przez interaktywne elementy
await userEvent.tab();
expect(document.activeElement).toBe(
screen.getByLabelText('Quantity for Widget')
);
});
});
> Utwórz testy API dla naszych endpointów REST włączając:
> - Uwierzytelnianie
> - Operacje CRUD
> - Obsługę błędów
> - Rate limiting
> - Walidację odpowiedzi
import request from 'supertest';
import app from '../app';
import { createTestUser, generateToken } from './helpers';
describe('User API', () => {
let authToken;
let testUser;
beforeAll(async () => {
testUser = await createTestUser();
authToken = generateToken(testUser);
});
describe('GET /api/users/:id', () => {
it('returns user data for authenticated requests', async () => {
const response = await request(app)
.get(`/api/users/${testUser.id}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body).toMatchObject({
id: testUser.id,
email: testUser.email,
name: testUser.name
});
expect(response.body).not.toHaveProperty('password');
});
it('returns 401 without authentication', async () => {
await request(app)
.get(`/api/users/${testUser.id}`)
.expect(401);
});
it('returns 404 for non-existent user', async () => {
await request(app)
.get('/api/users/99999')
.set('Authorization', `Bearer ${authToken}`)
.expect(404)
.expect(res => {
expect(res.body.error).toBe('User not found');
});
});
it('validates response schema', async () => {
const response = await request(app)
.get(`/api/users/${testUser.id}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
// Claude uwzględnia walidację schematu
expect(response.body).toMatchSchema({
type: 'object',
required: ['id', 'email', 'name', 'createdAt'],
properties: {
id: { type: 'number' },
email: { type: 'string', format: 'email' },
name: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' }
}
});
});
});
describe('Rate Limiting', () => {
it('enforces rate limits', async () => {
const requests = Array(101).fill(null).map(() =>
request(app)
.get('/api/users')
.set('Authorization', `Bearer ${authToken}`)
);
const responses = await Promise.all(requests);
const rateLimited = responses.filter(r => r.status === 429);
expect(rateLimited.length).toBeGreaterThan(0);
expect(rateLimited[0].headers['x-ratelimit-limit']).toBe('100');
expect(rateLimited[0].headers['retry-after']).toBeDefined();
});
});
});
> Utwórz testy obciążeniowe dla naszego API które symulują:
> - 1000 jednoczesnych użytkowników
> - Mieszane operacje read/write
> - Stopniowe rozbieganie
> - Testowanie skoków
> - Testowanie wytrzymałości przez 1 godzinę

Claude generuje:

// skrypt testu obciążenia k6
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Niestandardowe metryki
const errorRate = new Rate('errors');
const apiLatency = new Trend('api_latency');
export const options = {
scenarios: {
// Stopniowe rozbieganie
gradual_load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '5m', target: 100 },
{ duration: '10m', target: 500 },
{ duration: '10m', target: 1000 },
{ duration: '5m', target: 0 },
],
},
// Test skoków
spike_test: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '30s', target: 2000 },
{ duration: '2m', target: 100 },
{ duration: '30s', target: 0 },
],
startTime: '30m',
},
// Test wytrzymałości
endurance: {
executor: 'constant-vus',
vus: 200,
duration: '1h',
startTime: '35m',
},
},
thresholds: {
http_req_duration: ['p(95) < 500'], // 95% żądań poniżej 500ms
errors: ['rate < 0.01'], // Wskaźnik błędów poniżej 1%
},
};
export default function () {
const userId = Math.floor(Math.random() * 10000);
// Mieszane operacje oparte na realistycznym użyciu
const operation = Math.random();
if (operation < 0.7) {
// 70% operacji odczytu
const response = http.get(
`${__ENV.API_URL}/api/users/${userId}`,
{ headers: { Authorization: `Bearer ${__ENV.TOKEN}` } }
);
check(response, {
'read status is 200': (r) => r.status === 200,
'read response time < 200ms': (r) => r.timings.duration < 200,
});
errorRate.add(response.status !== 200);
apiLatency.add(response.timings.duration);
} else if (operation < 0.9) {
// 20% operacji zapisu
const payload = JSON.stringify({
name: `User ${userId}`,
email: `user${userId}@example.com`,
});
const response = http.put(
`${__ENV.API_URL}/api/users/${userId}`,
payload,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${__ENV.TOKEN}`,
},
}
);
check(response, {
'write status is 200': (r) => r.status === 200,
'write response time < 500ms': (r) => r.timings.duration < 500,
});
errorRate.add(response.status !== 200);
apiLatency.add(response.timings.duration);
} else {
// 10% złożonych operacji
const batch = http.batch([
['GET', `${__ENV.API_URL}/api/users/${userId}/orders`],
['GET', `${__ENV.API_URL}/api/users/${userId}/preferences`],
['GET', `${__ENV.API_URL}/api/users/${userId}/history`],
]);
batch.forEach(response => {
errorRate.add(response.status !== 200);
apiLatency.add(response.timings.duration);
});
}
sleep(Math.random() * 3 + 1); // Losowy czas myślenia 1-4s
}
export function handleSummary(data) {
return {
'summary.html': htmlReport(data),
'summary.json': JSON.stringify(data),
};
}
> Skonfiguruj kompleksowy pipeline testowy CI/CD który:
> - Uruchamia się przy każdym commit
> - Równoległe wykonywanie testów
> - Generuje raporty pokrycia
> - Uruchamia różne zestawy testów na podstawie zmian
> - Powiadamia o niepowodzeniach
> - Blokuje deployment jeśli bramy jakości nie przechodzą

Claude tworzy:

.github/workflows/test-pipeline.yml
name: Comprehensive Test Pipeline
on:
pull_request:
types: [opened, synchronize]
push:
branches: [main, develop]
jobs:
# Szybkie testy uruchamiają się pierwsze
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
shard: [1, 2, 3, 4] # Równoległe wykonanie
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests (shard ${{ matrix.shard }}/4)
run: |
npm test -- \
--shard=${{ matrix.shard }}/4 \
--coverage \
--coverageReporters=json
- name: Upload coverage
uses: actions/upload-artifact@v3
with:
name: coverage-${{ matrix.node }}-${{ matrix.shard }}
path: coverage/coverage-final.json
# Testy integracyjne po przejściu testów jednostkowych
integration-tests:
needs: unit-tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: testpass
options: >-
--health-cmd pg_isready
--health-interval 10s
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install and build
run: |
npm ci
npm run build
- name: Run integration tests
env:
DATABASE_URL: postgresql://postgres:testpass@localhost/test
REDIS_URL: redis://localhost
run: npm run test:integration
# Testy E2E tylko na main branch
e2e-tests:
if: github.ref == 'refs/heads/main'
needs: [unit-tests, integration-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run E2E tests
run: npm run test:e2e
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v3
with:
name: playwright-traces
path: test-results/
# Agreguj pokrycie i sprawdź bramy jakości
quality-gates:
needs: [unit-tests, integration-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download all coverage reports
uses: actions/download-artifact@v3
with:
pattern: coverage-*
path: coverage/
- name: Merge coverage reports
run: |
npx nyc merge coverage coverage/merged.json
npx nyc report \
--reporter=text \
--reporter=html \
--reporter=json-summary \
--temp-dir=coverage
- name: Check coverage thresholds
run: |
node scripts/check-coverage.js \
--statements=80 \
--branches=75 \
--functions=80 \
--lines=80
- name: Comment PR with coverage
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const coverage = require('./coverage/coverage-summary.json');
const comment = generateCoverageComment(coverage);
github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: comment
});
// Claude sugeruje tę strukturę
describe('PaymentService', () => {
// Grupuj według funkcjonalności
describe('processPayment', () => {
// Grupuj według scenariusza
describe('with valid card', () => {
it('charges the correct amount');
it('returns transaction ID');
it('sends receipt email');
});
describe('with invalid card', () => {
it('throws appropriate error');
it('does not charge customer');
it('logs failed attempt');
});
});
});
> Utwórz system factory danych testowych który generuje:
> - Realistyczne profile użytkowników
> - Prawidłowe/nieprawidłowe karty kredytowe
> - Historie zamówień
> - Dane przypadków granicznych

Claude może dostosować się do Twojego preferowanego stylu:

  • Expectations Jest
  • Asercje Chai
  • Should.js
  • Niestandardowe matchers
> Optymalizuj nasz zestaw testów. Trwa 20 minut.
> Zrób go szybszym bez utraty pokrycia

Nauczyłeś się jak Claude Code przekształca testowanie z obowiązku w supermoc. Z AI obsługującym boilerplate, przypadki graniczne i konfigurację frameworka, możesz skupić się na tym co naprawdę ważne: zapewnieniu że Twój kod działa poprawnie i pozostaje taki.

Pamiętaj: Testy to nie tylko łapanie błędów - to pewność. Pewność refaktoryzacji, wdrażania w piątek po południu, przekazywania kodu współpracownikom. Pozwól Claude Code pomóc Ci zbudować tę pewność przez kompleksowe, utrzymywalne zestawy testów, których napisanie ręczne zajęłoby dni.