50+ Backend Testing Pytania Rekrutacyjne 2026: Unit Tests, Integration Tests i TDD
Na rozmowie rekrutacyjnej pada pytanie: "Jak testujesz swój kod backend?". Kandydat odpowiada: "Testuję ręcznie przez Postman". To odpowiedź, która pokazuje brak profesjonalnego podejścia. W 2026 roku automatyczne testy to standard - bez nich żaden poważny zespół nie przyjmie kodu do production.
W tym przewodniku znajdziesz wszystko o testowaniu backend - od piramidy testów, przez unit i integration testy, po mockowanie, TDD i najlepsze praktyki. Każda sekcja zawiera konkretne przykłady kodu i pytania rekrutacyjne, które pomogą Ci przygotować się do rozmowy o jakość i profesjonalizm w testowaniu aplikacji backend.
Spis Treści
- Wprowadzenie do Testowania Backend Pytania
- Piramida Testów Pytania
- Unit Testing Pytania
- Mocking Pytania
- Integration Testing Pytania
- Testowanie Kodu Asynchronicznego Pytania
- TDD Pytania
- Code Coverage Pytania
- Najlepsze Praktyki Pytania
- Pytania Rekrutacyjne z Odpowiedziami
- Zadania Praktyczne
Wprowadzenie do Testowania Backend Pytania
Na rozmowie rekrutacyjnej możesz usłyszeć pytanie o podejście do testowania. Warto przygotować dwie wersje odpowiedzi - krótką (30 sekund) na wstępne pytania i rozszerzoną (2 minuty) na pogłębioną dyskusję.
Jak odpowiedzieć na pytanie o testowanie w 30 sekund?
Krótka odpowiedź powinna pokazać znajomość kluczowych konceptów testowania backend oraz umiejętność ich praktycznego zastosowania. Struktura "piramida testów → narzędzia → praktyki" sprawia, że odpowiedź jest kompletna i merytoryczna.
"Stosuję piramidę testów - dużo szybkich unit testów dla logiki biznesowej, testy integracyjne dla API i bazy danych, oraz nieliczne E2E dla krytycznych ścieżek. Unit testy piszę w Jest, mockując zewnętrzne zależności. Integration testy używają testowej bazy danych z rollback po każdym teście. API testuję przez Supertest. Staram się stosować TDD dla nowych funkcjonalności - najpierw test, potem implementacja."
Jak szczegółowo opisać swoje podejście do testowania w 2 minuty?
Szczegółowa odpowiedź powinna wykazać głębokie zrozumienie strategii testowania i umiejętność dostosowania podejścia do różnych scenariuszy. Struktura od fundamentów (piramida) przez konkretne narzędzia po metodologie (TDD) pokazuje kompleksowe myślenie o testowaniu.
Testowanie backend opiera się na piramidzie testów, która definiuje proporcje między różnymi typami testów.
Na dole piramidy są unit testy - testują pojedyncze funkcje lub klasy w izolacji. Mockuję wszystkie zależności zewnętrzne: bazę danych, API, system plików. Dzięki temu testy są szybkie (milisekundy), deterministyczne i łatwe do debugowania. Unit testy weryfikują logikę biznesową - walidację, obliczenia, transformacje danych.
Środek piramidy to testy integracyjne. Testują współpracę komponentów - np. endpoint API z bazą danych. Używam testowej instancji bazy (docker-compose lub in-memory), a każdy test działa w transakcji, która jest rollback'owana. Testuję pełny flow: request → controller → service → repository → database → response.
Na szczycie piramidy są testy E2E - testują system z perspektywy użytkownika. Dla backend to mogą być testy całego API z prawdziwą bazą i zewnętrznymi serwisami (lub ich stubami). E2E są wolne i kruche, więc piszę je tylko dla krytycznych ścieżek.
Do testowania API używam Supertest - pozwala wykonywać żądania HTTP bez uruchamiania serwera. Testuję status codes, response body, nagłówki, error handling, autoryzację.
Mockowanie realizuję przez dependency injection - serwisy przyjmują zależności jako parametry, co ułatwia podmianę na mocki w testach. Używam jest.mock() dla modułów, jest.spyOn() dla śledzenia wywołań.
Staram się stosować TDD: piszę failing test, implementuję minimum kodu, refaktoryzuję. TDD wymusza lepszy design - kod musi być testowalny, co oznacza loose coupling i single responsibility.
Piramida Testów Pytania
Piramida testów to fundamentalna koncepcja w testowaniu oprogramowania, która definiuje optymalne proporcje między różnymi typami testów. Zrozumienie jej struktury jest kluczowe na rozmowie rekrutacyjnej.
Jak wygląda struktura piramidy testów?
Piramida testów wizualizuje hierarchię testów od najszybszych i najbardziej granularnych (unit testy na dole) do najwolniejszych i najbardziej kompleksowych (E2E na szczycie). Im wyżej w piramidzie, tym mniej testów powinno być, ale każdy test weryfikuje więcej komponentów jednocześnie.
Pełny system • Wolne, kruche"] INT["🟠 Integration Tests (20-30%)
API + Database • Średnia szybkość"] UNIT["🟢 Unit Tests (60-70%)
Izolowane funkcje • Szybkie, stabilne"] end E2E --> INT --> UNIT style E2E fill:#fecaca,stroke:#dc2626,color:#991b1b style INT fill:#fed7aa,stroke:#ea580c,color:#9a3412 style UNIT fill:#bbf7d0,stroke:#16a34a,color:#166534
Czym charakteryzuje się każdy poziom piramidy testów?
Każdy poziom piramidy ma swoje unikalne cechy, które determinują kiedy i jak go stosować. Unit testy są szybkie i izolowane, integration testy weryfikują współpracę komponentów, a E2E testują cały system end-to-end. Poniższa tabela pokazuje kluczowe różnice między poziomami.
| Aspekt | Unit | Integration | E2E |
|---|---|---|---|
| Zakres | Funkcja/klasa | Moduły + DB | Cały system |
| Szybkość | ms | sekundy | minuty |
| Izolacja | Pełna (mocki) | Częściowa | Brak |
| Stabilność | Bardzo wysoka | Wysoka | Niska |
| Koszt utrzymania | Niski | Średni | Wysoki |
| Co wykrywa | Błędy logiki | Błędy integracji | Błędy systemowe |
Kiedy używać którego poziomu testów?
Wybór poziomu testu zależy od tego, co chcemy zweryfikować. Unit testy stosujemy dla czystej logiki biznesowej bez zależności zewnętrznych. Integration testy używamy gdy chcemy sprawdzić współpracę API z bazą danych. E2E rezerwujemy dla krytycznych ścieżek biznesowych, gdzie liczy się pełny flow użytkownika.
// UNIT TEST - czysta logika biznesowa
function calculateDiscount(price, userType) {
if (userType === 'premium') return price * 0.8;
if (userType === 'regular') return price * 0.95;
return price;
}
// Test nie potrzebuje bazy, API, niczego zewnętrznego
// INTEGRATION TEST - API + database
// POST /orders → tworzy zamówienie w bazie → zwraca order ID
// Test musi użyć prawdziwej (testowej) bazy
// E2E TEST - krytyczna ścieżka biznesowa
// Rejestracja → logowanie → złożenie zamówienia → płatność
// Test całego flow użytkownika
Unit Testing Pytania
Unit testy to fundament każdej strategii testowania. Testują pojedyncze jednostki kodu (funkcje, klasy) w izolacji od reszty systemu, co zapewnia szybkie wykonanie i łatwe debugowanie.
Jak wyglądają podstawy testowania w Jest?
Jest to najpopularniejszy framework do testowania JavaScript. Oferuje intuicyjne API z funkcjami describe (grupowanie testów), it/test (pojedynczy test) i expect (asercje). Testy w Jest są czytelne i łatwe w utrzymaniu dzięki bogatemu zestawowi matcherów.
// math.js
export function add(a, b) {
return a + b;
}
export function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// math.test.js
import { add, divide } from './math.js';
describe('Math functions', () => {
describe('add', () => {
it('adds two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('handles negative numbers', () => {
expect(add(-1, -2)).toBe(-3);
});
it('handles zero', () => {
expect(add(0, 5)).toBe(5);
});
});
describe('divide', () => {
it('divides two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
Jak testować klasę z zależnościami?
W rzeczywistych aplikacjach klasy mają zależności - repozytoria, serwisy zewnętrzne, klienty API. W unit testach te zależności zastępujemy mockami, aby testować tylko logikę naszej klasy w izolacji. Dependency injection ułatwia ten proces - zależności przekazujemy przez konstruktor.
// userService.js
export class UserService {
constructor(userRepository, emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
async createUser(userData) {
// Walidacja
if (!userData.email) {
throw new Error('Email is required');
}
// Sprawdź czy user istnieje
const existing = await this.userRepository.findByEmail(userData.email);
if (existing) {
throw new Error('User already exists');
}
// Utwórz usera
const user = await this.userRepository.create(userData);
// Wyślij email powitalny
await this.emailService.sendWelcome(user.email, user.name);
return user;
}
}
// userService.test.js
import { UserService } from './userService.js';
describe('UserService', () => {
let userService;
let mockUserRepository;
let mockEmailService;
beforeEach(() => {
// Tworzymy mocki
mockUserRepository = {
findByEmail: jest.fn(),
create: jest.fn()
};
mockEmailService = {
sendWelcome: jest.fn()
};
// Wstrzykujemy mocki
userService = new UserService(mockUserRepository, mockEmailService);
});
describe('createUser', () => {
const validUserData = {
email: 'test@example.com',
name: 'John Doe'
};
it('creates user and sends welcome email', async () => {
// Arrange
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue({
id: 1,
...validUserData
});
mockEmailService.sendWelcome.mockResolvedValue(true);
// Act
const result = await userService.createUser(validUserData);
// Assert
expect(result).toEqual({ id: 1, ...validUserData });
expect(mockUserRepository.findByEmail).toHaveBeenCalledWith('test@example.com');
expect(mockUserRepository.create).toHaveBeenCalledWith(validUserData);
expect(mockEmailService.sendWelcome).toHaveBeenCalledWith(
'test@example.com',
'John Doe'
);
});
it('throws error if email is missing', async () => {
await expect(userService.createUser({ name: 'John' }))
.rejects
.toThrow('Email is required');
// Upewniamy się, że nie próbowano tworzyć usera
expect(mockUserRepository.create).not.toHaveBeenCalled();
});
it('throws error if user already exists', async () => {
mockUserRepository.findByEmail.mockResolvedValue({ id: 1 });
await expect(userService.createUser(validUserData))
.rejects
.toThrow('User already exists');
expect(mockUserRepository.create).not.toHaveBeenCalled();
expect(mockEmailService.sendWelcome).not.toHaveBeenCalled();
});
});
});
Jakie są najważniejsze matchers w Jest?
Matchers to metody używane z expect() do weryfikacji wartości. Jest oferuje bogaty zestaw matcherów - od prostego porównania (toBe) przez deep equality (toEqual) po specjalizowane dla tablic, obiektów i funkcji asynchronicznych. Znajomość tych matcherów znacząco przyspiesza pisanie testów.
// Equality
expect(value).toBe(5); // ===
expect(obj).toEqual({ a: 1 }); // deep equality
expect(obj).toStrictEqual({ a: 1 }); // deep equality + undefined props
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(5);
expect(0.1 + 0.2).toBeCloseTo(0.3); // floating point
// Strings
expect(str).toMatch(/regex/);
expect(str).toContain('substring');
// Arrays
expect(arr).toContain(item);
expect(arr).toContainEqual({ a: 1 });
expect(arr).toHaveLength(3);
// Objects
expect(obj).toHaveProperty('key');
expect(obj).toHaveProperty('nested.key', 'value');
expect(obj).toMatchObject({ partial: 'match' });
// Exceptions
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('message');
expect(() => fn()).toThrow(ErrorClass);
// Async
await expect(promise).resolves.toBe(value);
await expect(promise).rejects.toThrow('error');
// Mock functions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(arg1, arg2);
expect(mockFn).toHaveBeenLastCalledWith(arg);
expect(mockFn).toHaveReturnedWith(value);
Mocking Pytania
Mocking to technika zastępowania rzeczywistych zależności kontrolowanymi zamiennikami w testach. Pozwala izolować testowany kod, kontrolować zachowanie zależności i weryfikować interakcje między komponentami.
Czym różnią się Mock, Stub, Spy i Fake?
Test doubles (zamienniki testowe) dzielą się na cztery główne typy, każdy z innym zastosowaniem. Mock pozwala weryfikować wywołania, stub zwraca predefiniowane dane, spy obserwuje prawdziwą implementację, a fake to uproszczona działająca implementacja. Wybór zależy od tego, co chcemy testować.
// MOCK - weryfikujemy wywołania
const mockSendEmail = jest.fn();
mockSendEmail.mockResolvedValue(true);
await sendWelcomeEmail(mockSendEmail, 'user@example.com');
expect(mockSendEmail).toHaveBeenCalledWith(
'user@example.com',
expect.stringContaining('Welcome')
);
// STUB - zwracamy predefiniowane dane
const stubUserRepository = {
findById: async (id) => ({ id, name: 'John', email: 'john@example.com' })
};
// SPY - obserwujemy prawdziwą implementację
const realService = new EmailService();
const spy = jest.spyOn(realService, 'send');
await realService.send('test@example.com', 'Hello');
expect(spy).toHaveBeenCalledWith('test@example.com', 'Hello');
spy.mockRestore(); // Przywróć oryginalną implementację
// FAKE - działająca uproszczona implementacja
class FakeUserRepository {
constructor() {
this.users = new Map();
this.nextId = 1;
}
async create(data) {
const user = { id: this.nextId++, ...data };
this.users.set(user.id, user);
return user;
}
async findById(id) {
return this.users.get(id) || null;
}
async findByEmail(email) {
return [...this.users.values()].find(u => u.email === email) || null;
}
}
Jak mockować moduły w Jest?
Jest oferuje potężny mechanizm jest.mock() do zastępowania całych modułów. Możesz zamockować cały moduł, zachować część oryginalnej implementacji za pomocą jest.requireActual(), lub mockować zewnętrzne pakiety z node_modules. To kluczowe dla izolacji testów od zewnętrznych zależności.
// Mockowanie całego modułu
jest.mock('./emailService', () => ({
sendEmail: jest.fn().mockResolvedValue(true),
sendBulk: jest.fn().mockResolvedValue({ sent: 10, failed: 0 })
}));
// Mockowanie z zachowaniem części implementacji
jest.mock('./utils', () => ({
...jest.requireActual('./utils'),
fetchExternalData: jest.fn().mockResolvedValue({ data: 'mocked' })
}));
// Mockowanie node_modules
jest.mock('axios', () => ({
get: jest.fn(),
post: jest.fn()
}));
import axios from 'axios';
axios.get.mockResolvedValue({ data: { users: [] } });
Jak mockować timery w testach?
Testowanie kodu z setTimeout, setInterval czy Date wymaga kontroli nad czasem. Jest oferuje jest.useFakeTimers(), który pozwala symulować upływ czasu bez rzeczywistego czekania. Możesz przewijać czas o określoną wartość lub wykonać wszystkie zaplanowane timery natychmiast.
describe('Delayed operations', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('executes callback after delay', () => {
const callback = jest.fn();
scheduleTask(callback, 1000);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
});
it('can fast-forward all timers', async () => {
const callback = jest.fn();
setTimeout(callback, 5000);
jest.runAllTimers();
expect(callback).toHaveBeenCalled();
});
});
Integration Testing Pytania
Testy integracyjne weryfikują współpracę między komponentami systemu - API z bazą danych, serwisami zewnętrznymi czy innymi modułami. Są wolniejsze niż unit testy, ale wykrywają problemy na styku komponentów.
Jak testować API z Supertest?
Supertest to biblioteka do testowania HTTP w Node.js. Pozwala wykonywać żądania do aplikacji Express bez uruchamiania serwera na prawdziwym porcie. Możesz testować status codes, response body, headers i autoryzację w czytelny, deklaratywny sposób.
// app.js
import express from 'express';
import { userRouter } from './routes/user.js';
export const app = express();
app.use(express.json());
app.use('/api/users', userRouter);
// user.integration.test.js
import request from 'supertest';
import { app } from './app.js';
import { db } from './database.js';
describe('User API', () => {
beforeAll(async () => {
await db.connect();
});
afterAll(async () => {
await db.disconnect();
});
beforeEach(async () => {
await db.clear(); // Wyczyść dane przed każdym testem
});
describe('POST /api/users', () => {
it('creates a new user', async () => {
const userData = {
email: 'test@example.com',
name: 'John Doe',
password: 'securePassword123'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(Number),
email: 'test@example.com',
name: 'John Doe'
});
expect(response.body).not.toHaveProperty('password');
});
it('returns 400 for invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
name: 'John',
password: 'password123'
})
.expect(400);
expect(response.body).toEqual({
error: 'Validation failed',
details: expect.arrayContaining([
expect.objectContaining({ field: 'email' })
])
});
});
it('returns 409 for duplicate email', async () => {
const userData = {
email: 'existing@example.com',
name: 'First User',
password: 'password123'
};
// Pierwszy user
await request(app).post('/api/users').send(userData).expect(201);
// Próba utworzenia duplikatu
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(409);
expect(response.body.error).toBe('User already exists');
});
});
describe('GET /api/users/:id', () => {
it('returns user by ID', async () => {
// Arrange: utwórz usera
const createResponse = await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
name: 'John Doe',
password: 'password123'
});
const userId = createResponse.body.id;
// Act & Assert
const response = await request(app)
.get(`/api/users/${userId}`)
.expect(200);
expect(response.body).toEqual({
id: userId,
email: 'test@example.com',
name: 'John Doe'
});
});
it('returns 404 for non-existent user', async () => {
await request(app)
.get('/api/users/99999')
.expect(404);
});
});
describe('Authentication', () => {
it('protects routes requiring auth', async () => {
await request(app)
.get('/api/users/me')
.expect(401);
});
it('allows access with valid token', async () => {
// Utwórz usera i zaloguj
await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
name: 'John',
password: 'password123'
});
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
const token = loginResponse.body.token;
// Użyj tokenu
const response = await request(app)
.get('/api/users/me')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.email).toBe('test@example.com');
});
});
});
Jak testować z bazą danych?
Testy integracyjne z bazą danych wymagają izolacji - każdy test powinien działać na czystych danych. Popularne strategie to: in-memory database (MongoDB Memory Server, SQLite), transakcje z rollback po każdym teście, lub czyszczenie danych przed/po testach. Wybór zależy od szybkości vs realizmu.
// Konfiguracja testowej bazy (setup.js)
import { MongoMemoryServer } from 'mongodb-memory-server';
import mongoose from 'mongoose';
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
await mongoose.connect(uri);
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});
// Alternatywnie: transakcje dla SQL
describe('Order Service', () => {
let transaction;
beforeEach(async () => {
transaction = await db.beginTransaction();
});
afterEach(async () => {
await transaction.rollback(); // Cofnij wszystkie zmiany
});
it('creates order with items', async () => {
const order = await orderService.create(
{ userId: 1, items: [{ productId: 1, quantity: 2 }] },
transaction
);
expect(order.id).toBeDefined();
expect(order.items).toHaveLength(1);
});
});
Czym są Test Fixtures i Factories?
Fixtures to predefiniowane dane testowe, a factories to funkcje generujące te dane dynamicznie. Factories z biblioteką Faker pozwalają tworzyć realistyczne, losowe dane dla każdego testu, eliminując problem duplikacji i ułatwiając tworzenie skomplikowanych struktur danych z relacjami.
// factories/user.factory.js
import { faker } from '@faker-js/faker';
export function createUserData(overrides = {}) {
return {
email: faker.internet.email(),
name: faker.person.fullName(),
password: 'Password123!',
role: 'user',
...overrides
};
}
export async function createUser(repository, overrides = {}) {
const data = createUserData(overrides);
return repository.create(data);
}
// factories/order.factory.js
export async function createOrderWithItems(
orderRepository,
productRepository,
{ userId, itemCount = 2 } = {}
) {
const products = await Promise.all(
Array.from({ length: itemCount }, () =>
productRepository.create({
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price())
})
)
);
return orderRepository.create({
userId,
items: products.map(p => ({
productId: p.id,
quantity: faker.number.int({ min: 1, max: 5 }),
price: p.price
}))
});
}
// Użycie w testach
describe('Order API', () => {
it('calculates order total correctly', async () => {
const user = await createUser(userRepository);
const order = await createOrderWithItems(orderRepository, productRepository, {
userId: user.id,
itemCount: 3
});
const response = await request(app)
.get(`/api/orders/${order.id}`)
.set('Authorization', `Bearer ${await getTokenForUser(user)}`);
const expectedTotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
expect(response.body.total).toBeCloseTo(expectedTotal);
});
});
Testowanie Kodu Asynchronicznego Pytania
Kod asynchroniczny (Promises, async/await) wymaga specjalnego podejścia w testach. Jest obsługuje async/await natywnie, ale trzeba pamiętać o poprawnym czekaniu na rezultaty i obsłudze błędów.
Jak testować async/await?
Jest automatycznie czeka na rozwiązanie Promise, jeśli funkcja testowa jest async i używa await. Dla testowania rejected promises używamy rejects.toThrow() lub bloku try/catch z expect.assertions() do weryfikacji, że asercja została wykonana.
describe('Async operations', () => {
it('fetches user data', async () => {
const user = await userService.getById(1);
expect(user.name).toBe('John');
});
it('handles async errors', async () => {
await expect(userService.getById(999))
.rejects
.toThrow('User not found');
});
// Alternatywna składnia dla rejected promises
it('handles async errors (alternative)', async () => {
expect.assertions(1); // Upewnij się, że asercja została wykonana
try {
await userService.getById(999);
} catch (error) {
expect(error.message).toBe('User not found');
}
});
});
Jak testować równoległe operacje?
Operacje równoległe (Promise.all, Promise.allSettled) wymagają testowania zarówno scenariuszy sukcesu jak i częściowych błędów. W testach symulujemy różne kombinacje resolved i rejected promises, aby zweryfikować poprawną obsługę wszystkich przypadków.
describe('Parallel operations', () => {
it('processes multiple items concurrently', async () => {
const items = [1, 2, 3, 4, 5];
const results = await Promise.all(
items.map(id => service.processItem(id))
);
expect(results).toHaveLength(5);
results.forEach(result => {
expect(result.status).toBe('processed');
});
});
it('handles partial failures in batch', async () => {
mockProcessor.process
.mockResolvedValueOnce({ success: true })
.mockRejectedValueOnce(new Error('Failed'))
.mockResolvedValueOnce({ success: true });
const result = await batchService.processAll([1, 2, 3]);
expect(result.successful).toBe(2);
expect(result.failed).toBe(1);
expect(result.errors[0]).toEqual({
itemId: 2,
error: 'Failed'
});
});
});
Jak testować logikę retry?
Mechanizmy retry (ponowne próby po błędzie) są powszechne w komunikacji z zewnętrznymi serwisami. W testach symulujemy sekwencję błędów i sukcesu za pomocą mockResolvedValueOnce/mockRejectedValueOnce, weryfikując liczbę prób i zachowanie po wyczerpaniu limitu.
describe('Retry mechanism', () => {
it('retries failed operations', async () => {
const mockOperation = jest.fn()
.mockRejectedValueOnce(new Error('Temporary failure'))
.mockRejectedValueOnce(new Error('Temporary failure'))
.mockResolvedValueOnce({ data: 'success' });
const result = await retryWithBackoff(mockOperation, {
maxRetries: 3,
initialDelay: 100
});
expect(result).toEqual({ data: 'success' });
expect(mockOperation).toHaveBeenCalledTimes(3);
});
it('throws after max retries exceeded', async () => {
const mockOperation = jest.fn()
.mockRejectedValue(new Error('Persistent failure'));
await expect(
retryWithBackoff(mockOperation, { maxRetries: 3 })
).rejects.toThrow('Persistent failure');
expect(mockOperation).toHaveBeenCalledTimes(3);
});
});
TDD Pytania
Test-Driven Development to metodologia, w której testy piszemy przed implementacją. Wymusza to przemyślenie interfejsu i oczekiwanego zachowania przed napisaniem kodu, co prowadzi do lepszego designu i mniejszej liczby błędów.
Jak wygląda cykl Red-Green-Refactor?
Cykl TDD składa się z trzech kroków powtarzanych iteracyjnie. Najpierw piszemy failing test (Red), następnie piszemy minimum kodu aby test przeszedł (Green), na końcu poprawiamy strukturę kodu zachowując testy zielone (Refactor). Ten rytm wymusza małe, przemyślane kroki.
// 1. RED - Napisz failing test
describe('PasswordValidator', () => {
it('requires minimum 8 characters', () => {
expect(validatePassword('short')).toEqual({
valid: false,
errors: ['Password must be at least 8 characters']
});
});
});
// Test fails: validatePassword is not defined
// 2. GREEN - Napisz minimum kodu by test przeszedł
function validatePassword(password) {
const errors = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
return {
valid: errors.length === 0,
errors
};
}
// Test passes!
// 3. Dodaj kolejny test
it('requires at least one uppercase letter', () => {
expect(validatePassword('lowercase1')).toEqual({
valid: false,
errors: ['Password must contain at least one uppercase letter']
});
});
// 4. Rozszerz implementację
function validatePassword(password) {
const errors = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
return {
valid: errors.length === 0,
errors
};
}
// 5. REFACTOR - popraw strukturę zachowując testy zielone
const PASSWORD_RULES = [
{
test: (p) => p.length >= 8,
message: 'Password must be at least 8 characters'
},
{
test: (p) => /[A-Z]/.test(p),
message: 'Password must contain at least one uppercase letter'
},
{
test: (p) => /[a-z]/.test(p),
message: 'Password must contain at least one lowercase letter'
},
{
test: (p) => /[0-9]/.test(p),
message: 'Password must contain at least one digit'
}
];
function validatePassword(password) {
const errors = PASSWORD_RULES
.filter(rule => !rule.test(password))
.map(rule => rule.message);
return {
valid: errors.length === 0,
errors
};
}
Jak stosować TDD dla nowej funkcjonalności?
TDD sprawdza się szczególnie dobrze przy implementacji nowych funkcjonalności od zera. Zaczynam od najprostszego przypadku (test 1), implementuję minimum kodu, dodaję kolejny test dla bardziej złożonego przypadku, rozszerzam implementację. Ten proces naturalnie buduje kompletne rozwiązanie krok po kroku.
// Scenariusz: Implementacja systemu rabatów
// Test 1: Brak rabatu dla nowego klienta
describe('DiscountCalculator', () => {
it('returns 0% discount for new customers', () => {
const customer = { orderCount: 0, totalSpent: 0 };
expect(calculateDiscount(customer)).toBe(0);
});
// Test 2: 5% dla lojalnych klientów
it('returns 5% discount for customers with 5+ orders', () => {
const customer = { orderCount: 5, totalSpent: 500 };
expect(calculateDiscount(customer)).toBe(5);
});
// Test 3: 10% dla VIP
it('returns 10% discount for VIP customers (spent > 1000)', () => {
const customer = { orderCount: 10, totalSpent: 1500 };
expect(calculateDiscount(customer)).toBe(10);
});
// Test 4: Maksymalny rabat
it('caps discount at 15%', () => {
const customer = { orderCount: 100, totalSpent: 50000, isPremium: true };
expect(calculateDiscount(customer)).toBe(15);
});
// Test 5: Premium override
it('gives premium customers minimum 10% discount', () => {
const customer = { orderCount: 1, totalSpent: 50, isPremium: true };
expect(calculateDiscount(customer)).toBe(10);
});
});
Code Coverage Pytania
Code coverage (pokrycie kodu testami) to metryka pokazująca, jaki procent kodu jest wykonywany podczas testów. Pomaga identyfikować nietestawane fragmenty kodu, ale nie jest miarą jakości testów - można mieć 100% coverage z bezwartościowymi testami.
Jak skonfigurować code coverage w Jest?
Jest ma wbudowane wsparcie dla coverage bez dodatkowych bibliotek. Konfiguracja w jest.config.js pozwala określić które pliki mierzyć, minimalne progi akceptacji (threshold) oraz format raportów. Progi można ustawić globalnie lub per-katalog dla krytycznych części aplikacji.
// jest.config.js
export default {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/services/': {
branches: 90,
functions: 90
}
}
};
Jak interpretować raporty code coverage?
Raport coverage pokazuje cztery główne metryki dla każdego pliku i całego projektu. Statements to procent wykonanych instrukcji, Branches pokazuje pokrycie rozgałęzień (if/else, switch), Functions to procent wywołanych funkcji, a Lines to procent wykonanych linii kodu. Warto analizować niskie wartości Branches - często wskazują na nietestawane ścieżki błędów.
----------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------------------|---------|----------|---------|---------|
All files | 85.23 | 78.45 | 89.12 | 84.56 |
src/services | 92.34 | 88.21 | 95.45 | 91.78 |
userService.js | 95.00 | 90.00 | 100.00 | 94.44 |
orderService.js | 89.47 | 86.36 | 90.91 | 89.13 |
src/controllers | 78.12 | 68.75 | 82.35 | 77.27 |
----------------------|---------|----------|---------|---------|
- Statements - % wykonanych instrukcji
- Branches - % pokrytych gałęzi (if/else, switch, ternary)
- Functions - % wywołanych funkcji
- Lines - % wykonanych linii
Dlaczego wysokie coverage nie gwarantuje jakości testów?
Coverage mierzy tylko czy kod został wykonany, nie czy został właściwie przetestowany. Można osiągnąć 100% coverage testami, które nie sprawdzają edge cases, niepoprawnych danych wejściowych czy boundary conditions. Poniższy przykład pokazuje, jak test dający pełne pokrycie może być bezwartościowy - wszystkie linie są wykonane, ale wiele scenariuszy pozostaje niesprawdzonych.
// 100% coverage, ale słaby test
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// Ten test daje 100% coverage...
it('works', () => {
expect(divide(10, 2)).toBe(5);
expect(() => divide(1, 0)).toThrow();
});
// ...ale nie testuje edge cases:
// - Ujemne liczby
// - Ułamki
// - Bardzo duże liczby
// - NaN/Infinity
// Lepsze testy:
it('handles negative numbers', () => {
expect(divide(-10, 2)).toBe(-5);
expect(divide(10, -2)).toBe(-5);
expect(divide(-10, -2)).toBe(5);
});
it('handles floating point', () => {
expect(divide(1, 3)).toBeCloseTo(0.333, 3);
});
Najlepsze Praktyki Pytania
Dobre testy to nie tylko testy, które przechodzą. To testy czytelne, łatwe do utrzymania i dające jasny feedback gdy coś się psuje. Na rozmowie rekrutacyjnej pytania o best practices pokazują, czy kandydat pisze testy mechanicznie, czy rozumie dlaczego pewne wzorce działają lepiej.
Czym jest wzorzec AAA (Arrange-Act-Assert)?
AAA to standard struktury testu, który dzieli go na trzy fazy: Arrange (przygotowanie danych i mocków), Act (wykonanie testowanej operacji) i Assert (weryfikacja wyników). Ta struktura ułatwia czytanie i utrzymanie testów - od razu widać co test robi i czego oczekuje. Jest to szczególnie ważne gdy testy się psują - łatwo zidentyfikować problem.
it('should create order with correct total', async () => {
// Arrange - przygotowanie danych i mocków
const user = await createUser({ balance: 1000 });
const products = [
{ id: 1, price: 100 },
{ id: 2, price: 200 }
];
mockProductRepository.findByIds.mockResolvedValue(products);
// Act - wykonanie testowanej operacji
const order = await orderService.createOrder(user.id, [
{ productId: 1, quantity: 2 },
{ productId: 2, quantity: 1 }
]);
// Assert - weryfikacja wyników
expect(order.total).toBe(400); // 2*100 + 1*200
expect(order.items).toHaveLength(2);
expect(mockPaymentService.charge).toHaveBeenCalledWith(user.id, 400);
});
Jak nazywać testy, żeby były zrozumiałe?
Nazwa testu to jego dokumentacja - powinna jasno komunikować co jest testowane i jakie zachowanie jest oczekiwane. Dobra nazwa pozwala zrozumieć błąd bez czytania kodu testu. Popularna konwencja to "should + oczekiwane zachowanie" lub "opisowy rezultat gdy warunek". Unikaj nazw jak "works", "test 1" czy "handles error" - nie dają żadnej informacji.
// ŹLE - niejasne co testujemy
it('works', () => {});
it('test 1', () => {});
it('handles error', () => {});
// DOBRZE - opisowe nazwy
it('returns 404 when user does not exist', () => {});
it('sends welcome email after successful registration', () => {});
it('throws ValidationError when email format is invalid', () => {});
it('retries up to 3 times on network timeout', () => {});
// Konwencja: should + expected behavior
it('should calculate discount based on order history', () => {});
it('should reject passwords shorter than 8 characters', () => {});
Dlaczego izolacja testów jest tak ważna?
Izolacja oznacza, że każdy test jest niezależny - może być uruchamiany w dowolnej kolejności, sam lub równolegle z innymi. Testy zależne od siebie (gdzie jeden korzysta z danych utworzonych przez poprzedni) są kruche i trudne do debugowania. Gdy test failuje, nie wiadomo czy problem jest w nim, czy w poprzednim teście który utworzył złe dane.
// ŹLE - testy zależne od siebie
let userId;
it('creates user', async () => {
const user = await createUser({ email: 'test@example.com' });
userId = user.id; // Zapisujemy do użycia w następnym teście
});
it('fetches created user', async () => {
const user = await getUser(userId); // Zależy od poprzedniego testu!
expect(user.email).toBe('test@example.com');
});
// DOBRZE - każdy test jest niezależny
it('creates user', async () => {
const user = await createUser({ email: 'test@example.com' });
expect(user.id).toBeDefined();
});
it('fetches user by id', async () => {
// Tworzymy własne dane
const created = await createUser({ email: 'fetch@example.com' });
const fetched = await getUser(created.id);
expect(fetched.email).toBe('fetch@example.com');
});
Jak skutecznie testować edge cases?
Edge cases to graniczne przypadki - wartości zerowe, null, puste tablice, bardzo duże liczby, nieprawidłowe dane wejściowe. To właśnie one powodują większość bugów produkcyjnych, bo programiści skupiają się na happy path. Dobry test suite obejmuje: happy path, boundary conditions (granice zakresów), error cases (nieprawidłowe dane) i edge cases (nietypowe scenariusze).
describe('calculateAge', () => {
// Happy path
it('calculates age correctly', () => {
expect(calculateAge(new Date('1990-01-01'))).toBe(36);
});
// Edge cases
it('returns 0 for today birthday', () => {
const today = new Date();
expect(calculateAge(today)).toBe(0);
});
it('handles birthday not yet this year', () => {
const futureThisYear = new Date();
futureThisYear.setMonth(futureThisYear.getMonth() + 1);
futureThisYear.setFullYear(futureThisYear.getFullYear() - 30);
expect(calculateAge(futureThisYear)).toBe(29); // Nie 30!
});
it('handles leap year birthdays', () => {
expect(calculateAge(new Date('2000-02-29'))).toBeDefined();
});
// Error cases
it('throws for future dates', () => {
const future = new Date();
future.setFullYear(future.getFullYear() + 1);
expect(() => calculateAge(future)).toThrow('Birth date cannot be in the future');
});
it('throws for invalid date', () => {
expect(() => calculateAge('not-a-date')).toThrow('Invalid date');
});
});
Pytania Rekrutacyjne z Odpowiedziami
Najczęściej zadawane pytania rekrutacyjne o testowanie backend z przygotowanymi odpowiedziami, które możesz dostosować do swojego doświadczenia.
Czym różni się mock od stub?
Mock i stub to oba test doubles, ale mają różne cele. Stub to uproszczona implementacja, która zwraca predefiniowane dane. Używamy go, gdy potrzebujemy kontrolować dane wejściowe do testowanego kodu. Stub nie weryfikuje jak został użyty.
Mock to obiekt, na którym możemy weryfikować interakcje - czy funkcja została wywołana, ile razy, z jakimi argumentami. Używamy go, gdy chcemy sprawdzić, czy testowany kod poprawnie komunikuje się z zależnościami.
// Stub - kontrolujemy co zwraca
const stubRepo = { findById: () => ({ id: 1, name: 'John' }) };
// Mock - weryfikujemy wywołania
const mockEmailService = jest.fn();
await sendWelcome(mockEmailService, 'user@example.com');
expect(mockEmailService).toHaveBeenCalledWith('user@example.com', expect.any(String));
Jak testujesz kod, który zależy od czasu?
Testowanie kodu zależnego od czasu wymaga kontrolowania systemowego zegara lub wstrzykiwania funkcji czasu. Trzy główne podejścia to fake timers w Jest, dependency injection funkcji czasu, lub mockowanie globalnego Date obiektu.
- Fake timers - Jest pozwala kontrolować zegar systemowy:
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01'));
// Testy działają "1 stycznia 2024"
jest.advanceTimersByTime(1000); // Przewiń o sekundę
- Dependency injection - przekazuję funkcję zwracającą czas:
function createToken(getNow = () => new Date()) {
return { createdAt: getNow(), expiresAt: new Date(getNow().getTime() + 3600000) };
}
// W teście: createToken(() => new Date('2024-01-01'))
- Mockowanie Date - dla prostych przypadków:
jest.spyOn(global, 'Date').mockImplementation(() => new Date('2024-01-01'));
Jak zapewnić izolację testów integracyjnych z bazą danych?
Izolacja testów z bazą danych jest kluczowa dla przewidywalności i niezależności testów. Każdy test powinien rozpoczynać się na czystych danych i nie wpływać na inne testy.
- Transakcje z rollback - każdy test działa w transakcji, która jest cofana:
beforeEach(async () => { transaction = await db.beginTransaction(); });
afterEach(async () => { await transaction.rollback(); });
- Czyszczenie po teście - usuwamy dane po każdym teście:
afterEach(async () => {
await db.query('TRUNCATE users, orders CASCADE');
});
- Osobna baza per test - dla pełnej izolacji, ale wolniejsze.
- In-memory database - np. mongodb-memory-server, SQLite in-memory:
const mongoServer = await MongoMemoryServer.create();
Wybór zależy od szybkości vs realizmu. Transakcje są szybkie, ale nie testują COMMIT. In-memory jest szybka, ale może mieć inne zachowanie niż produkcja.
Kiedy używać unit testów, a kiedy integracyjnych?
Wybór między unit a integration testami zależy od tego, co chcesz testować i jakie masz cele. Unit testy są szybkie i izolowane, integration testy realistyczne ale wolniejsze.
Unit testy dla:
- Czystej logiki biznesowej (kalkulacje, walidacja, transformacje)
- Funkcji bez efektów ubocznych
- Algorytmów i struktur danych
- Gdy chcę szybki feedback (milisekundy)
Integration testy dla:
- API endpoints (request → response)
- Interakcji z bazą danych
- Komunikacji między serwisami
- Gdy chcę przetestować prawdziwy flow
Proporcje w piramidzie: 70% unit, 20% integration, 10% E2E.
Przykład: dla funkcji createOrder:
- Unit test: sprawdzam kalkulację sumy, walidację danych
- Integration test: sprawdzam czy order trafia do bazy, czy odpowiedź ma poprawny format
Co to jest TDD i jakie są jego zalety?
TDD (Test-Driven Development) to podejście, gdzie test piszemy PRZED implementacją. Składa się z trzech kroków: Red (napisz failing test), Green (napisz minimum kodu by test przeszedł), Refactor (popraw strukturę zachowując testy zielone).
Zalety:
- Lepszy design - testowalność wymusza loose coupling
- Dokumentacja - testy opisują oczekiwane zachowanie
- Pewność przy refaktoryzacji - testy chronią przed regresją
- Mniej bugów - wymusza przemyślenie edge cases
- Szybszy development - mniej debugowania
Wady:
- Wymaga praktyki i dyscypliny
- Wolniejszy start projektu
- Nie zawsze pasuje (eksploracyjne kodowanie, prototypy)
Jak testujesz zewnętrzne API?
Testowanie zewnętrznych API zależy od poziomu testu i wymagań dotyczących realizmu vs szybkości. Różne podejścia mają różne zalety i wady.
Unit testy - mockuję klienta HTTP:
jest.mock('axios');
axios.get.mockResolvedValue({ data: { weather: 'sunny' } });
Integration testy - kilka opcji:
- Stub server (MSW, nock) - interceptuje żądania:
nock('https://api.weather.com')
.get('/current')
.reply(200, { weather:
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
