Backend Testing - Pytania Rekrutacyjne i Przewodnik 2026

Sławomir Plamowski 25 min czytania
backend integration-testing jest nodejs pytania-rekrutacyjne rozmowa-rekrutacyjna testing unit-testing

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.

Jak odpowiedzieć na pytania o testowanie backend

Odpowiedź w 30 sekund

"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."

Odpowiedź w 2 minuty

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

Struktura piramidy

        /\
       /  \      E2E Tests (5-10%)
      /----\     - Pełny system
     /      \    - Wolne, kruche
    /--------\
   /          \  Integration Tests (20-30%)
  /            \ - API + Database
 /--------------\- Średnia szybkość
/                \
/------------------\  Unit Tests (60-70%)
                     - Izolowane funkcje
                     - Szybkie, stabilne

Charakterystyka każdego poziomu

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 który poziom?

// 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

Podstawy Jest

// 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');
    });
  });
});

Testowanie klasy z zależnościami

// 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();
    });
  });
});

Matchers w Jest

// 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

Mock, Stub, Spy, Fake

// 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;
  }
}

Mockowanie modułów

// 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: [] } });

Mockowanie timerów

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

Testowanie API z Supertest

// 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');
    });
  });
});

Testowanie z bazą danych

// 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);
  });
});

Test Fixtures i Factories

// 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

Async/Await

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');
    }
  });
});

Testowanie równoległych operacji

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'
    });
  });
});

Testowanie retry logic

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 - Test-Driven Development

Cykl Red-Green-Refactor

// 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
  };
}

TDD dla nowej funkcjonalności

// 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

Konfiguracja coverage

// 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
    }
  }
};

Interpretacja coverage

----------------------|---------|----------|---------|---------|
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

Coverage to nie wszystko

// 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

Struktura testu AAA

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);
});

Nazewnictwo testów

// Ź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', () => {});

Izolacja testów

// Ź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');
});

Testowanie edge cases

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

Pytanie 1: Czym różni się mock od stub?

Odpowiedź: 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));

Pytanie 2: Jak testujesz kod, który zależy od czasu?

Odpowiedź: Trzy podejścia:

  1. 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ę
  1. 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'))
  1. Mockowanie Date - dla prostych przypadków:
jest.spyOn(global, 'Date').mockImplementation(() => new Date('2024-01-01'));

Pytanie 3: Jak zapewnić izolację testów integracyjnych z bazą danych?

Odpowiedź: Trzy strategie izolacji:

  1. 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(); });
  1. Czyszczenie po teście - usuwamy dane po każdym teście:
afterEach(async () => {
  await db.query('TRUNCATE users, orders CASCADE');
});
  1. Osobna baza per test - dla pełnej izolacji, ale wolniejsze.
  2. 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.

Pytanie 4: Kiedy używać unit testów, a kiedy integracyjnych?

Odpowiedź: 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

Pytanie 5: Co to jest TDD i jakie są jego zalety?

Odpowiedź: TDD (Test-Driven Development) to podejście, gdzie test piszemy PRZED implementacją. Cykl:

  1. Red - napisz failing test
  2. Green - napisz minimum kodu by test przeszedł
  3. 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)

Pytanie 6: Jak testujesz zewnętrzne API?

Odpowiedź: Zależnie od poziomu testu:

Unit testy - mockuję klienta HTTP:

jest.mock('axios');
axios.get.mockResolvedValue({ data: { weather: 'sunny' } });

Integration testy - kilka opcji:

  1. Stub server (MSW, nock) - interceptuje żądania:
nock('https://api.weather.com')
  .get('/current')
  .reply(200, { weather: 'sunny' });
  1. Sandbox/test environment - jeśli API udostępnia
  2. Contract testing - Pact weryfikuje zgodność z kontraktem

E2E - używam prawdziwego API (lub dedykowanego test environment)

Testuję też error scenarios: timeout, 500, rate limiting, invalid response.

Pytanie 7: Jak mierzysz jakość testów (poza coverage)?

Odpowiedź: Coverage to metryka ilościowa, ale nie jakościowa. Dodatkowo sprawdzam:

  1. Mutation testing - narzędzia jak Stryker wprowadzają mutacje w kodzie (zmiana > na <, usunięcie linii). Jeśli testy nadal przechodzą, są słabe.
  2. Flaky tests ratio - ile testów jest niestabilnych (czasem pass, czasem fail)
  3. Test execution time - wolne testy = rzadziej uruchamiane
  4. Defect slip-through - ile bugów produkcyjnych nie zostało wykrytych przez testy
  5. Code review - czy testy testują zachowanie, czy implementację?
  6. Test maintenance cost - ile czasu spędzamy na naprawie testów przy zmianach

Pytanie 8: Jak testujesz middleware w Express?

Odpowiedź: Dwa podejścia:

  1. Izolowany test middleware:
const mockReq = { headers: { authorization: 'Bearer token123' } };
const mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() };
const mockNext = jest.fn();

await authMiddleware(mockReq, mockRes, mockNext);

expect(mockReq.user).toBeDefined();
expect(mockNext).toHaveBeenCalled();
  1. Test przez Supertest - middleware jako część route:
app.get('/protected', authMiddleware, (req, res) => res.json(req.user));

await request(app)
  .get('/protected')
  .set('Authorization', 'Bearer valid-token')
  .expect(200);

await request(app)
  .get('/protected')
  .expect(401);

Preferuję Supertest dla middleware, bo testuje realne zachowanie z request/response.

Pytanie 9: Jak organizujesz testy w dużym projekcie?

Odpowiedź: Struktura katalogów:

src/
  services/
    userService.js
    userService.test.js    # Unit testy obok kodu
  repositories/
    userRepository.js
tests/
  integration/
    api/
      users.test.js        # Integration testy osobno
      orders.test.js
  e2e/
    checkout.test.js       # E2E osobno
  fixtures/
    users.js               # Dane testowe
  factories/
    user.factory.js        # Factory functions
  helpers/
    auth.js                # Pomocnicze funkcje testowe
jest.config.js
jest.integration.config.js  # Osobna konfiguracja

Konwencje:

  • Unit testy: *.test.js obok kodu źródłowego
  • Integration: katalog tests/integration
  • Osobne npm scripts: npm test (unit), npm run test:integration, npm run test:e2e
  • CI pipeline uruchamia wszystkie poziomy

Pytanie 10: Jak testujesz WebSocket lub real-time functionality?

Odpowiedź: Dla WebSocket używam biblioteki socket.io-client w testach:

import { io } from 'socket.io-client';
import { createServer } from '../server';

describe('WebSocket', () => {
  let server, clientSocket;

  beforeAll((done) => {
    server = createServer();
    server.listen(3001, done);
  });

  beforeEach((done) => {
    clientSocket = io('http://localhost:3001');
    clientSocket.on('connect', done);
  });

  afterEach(() => {
    clientSocket.close();
  });

  afterAll(() => {
    server.close();
  });

  it('receives message broadcast', (done) => {
    clientSocket.on('message', (data) => {
      expect(data).toBe('Hello everyone');
      done();
    });

    clientSocket.emit('broadcast', 'Hello everyone');
  });

  it('joins room and receives room messages', async () => {
    const messagePromise = new Promise(resolve => {
      clientSocket.on('room-message', resolve);
    });

    clientSocket.emit('join-room', 'test-room');
    clientSocket.emit('room-broadcast', { room: 'test-room', message: 'Hello room' });

    const message = await messagePromise;
    expect(message).toBe('Hello room');
  });
});

Używam Promise zamiast callback done dla czytelności async/await.

Zadania praktyczne

Zadanie 1: Napisz testy dla serwisu

Masz serwis do zarządzania koszykiem. Napisz unit testy:

class CartService {
  constructor(productRepository, discountService) {
    this.productRepository = productRepository;
    this.discountService = discountService;
    this.items = [];
  }

  async addItem(productId, quantity) { /* ... */ }
  async removeItem(productId) { /* ... */ }
  async getTotal(couponCode) { /* ... */ }
}
Rozwiązanie
describe('CartService', () => {
  let cartService;
  let mockProductRepository;
  let mockDiscountService;

  beforeEach(() => {
    mockProductRepository = {
      findById: jest.fn()
    };
    mockDiscountService = {
      validateCoupon: jest.fn(),
      calculateDiscount: jest.fn()
    };
    cartService = new CartService(mockProductRepository, mockDiscountService);
  });

  describe('addItem', () => {
    it('adds product to cart', async () => {
      mockProductRepository.findById.mockResolvedValue({
        id: 1,
        name: 'Product',
        price: 100,
        stock: 10
      });

      await cartService.addItem(1, 2);

      expect(cartService.items).toContainEqual({
        productId: 1,
        quantity: 2,
        price: 100
      });
    });

    it('throws if product not found', async () => {
      mockProductRepository.findById.mockResolvedValue(null);

      await expect(cartService.addItem(999, 1))
        .rejects.toThrow('Product not found');
    });

    it('throws if quantity exceeds stock', async () => {
      mockProductRepository.findById.mockResolvedValue({
        id: 1, price: 100, stock: 5
      });

      await expect(cartService.addItem(1, 10))
        .rejects.toThrow('Insufficient stock');
    });

    it('increases quantity for existing item', async () => {
      mockProductRepository.findById.mockResolvedValue({
        id: 1, price: 100, stock: 10
      });

      await cartService.addItem(1, 2);
      await cartService.addItem(1, 3);

      expect(cartService.items).toHaveLength(1);
      expect(cartService.items[0].quantity).toBe(5);
    });
  });

  describe('getTotal', () => {
    beforeEach(async () => {
      mockProductRepository.findById
        .mockResolvedValueOnce({ id: 1, price: 100, stock: 10 })
        .mockResolvedValueOnce({ id: 2, price: 50, stock: 10 });

      await cartService.addItem(1, 2); // 200
      await cartService.addItem(2, 3); // 150
    });

    it('calculates total without coupon', async () => {
      const total = await cartService.getTotal();
      expect(total).toBe(350);
    });

    it('applies valid coupon discount', async () => {
      mockDiscountService.validateCoupon.mockResolvedValue(true);
      mockDiscountService.calculateDiscount.mockReturnValue(35); // 10%

      const total = await cartService.getTotal('SAVE10');

      expect(total).toBe(315);
      expect(mockDiscountService.validateCoupon)
        .toHaveBeenCalledWith('SAVE10');
    });

    it('ignores invalid coupon', async () => {
      mockDiscountService.validateCoupon.mockResolvedValue(false);

      const total = await cartService.getTotal('INVALID');

      expect(total).toBe(350);
      expect(mockDiscountService.calculateDiscount).not.toHaveBeenCalled();
    });
  });
});

Zadanie 2: Napisz integration test dla API

// API do testowania
POST /api/orders - tworzy zamówienie
GET /api/orders/:id - pobiera zamówienie
PATCH /api/orders/:id/status - zmienia status
Rozwiązanie
import request from 'supertest';
import { app } from '../app';
import { db } from '../database';
import { createUser, createProduct } from './factories';

describe('Orders API', () => {
  let user, product, authToken;

  beforeAll(async () => {
    await db.connect();
  });

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

  beforeEach(async () => {
    await db.clear();

    user = await createUser({ email: 'test@example.com' });
    product = await createProduct({ price: 100, stock: 10 });

    const loginRes = await request(app)
      .post('/api/auth/login')
      .send({ email: 'test@example.com', password: 'password123' });

    authToken = loginRes.body.token;
  });

  describe('POST /api/orders', () => {
    it('creates order with valid items', async () => {
      const response = await request(app)
        .post('/api/orders')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          items: [{ productId: product.id, quantity: 2 }]
        })
        .expect(201);

      expect(response.body).toMatchObject({
        id: expect.any(Number),
        status: 'pending',
        total: 200,
        items: [
          {
            productId: product.id,
            quantity: 2,
            price: 100
          }
        ]
      });

      // Verify in database
      const dbOrder = await db.orders.findById(response.body.id);
      expect(dbOrder).toBeDefined();
      expect(dbOrder.userId).toBe(user.id);
    });

    it('returns 400 for empty cart', async () => {
      const response = await request(app)
        .post('/api/orders')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ items: [] })
        .expect(400);

      expect(response.body.error).toBe('Cart cannot be empty');
    });

    it('returns 401 without auth', async () => {
      await request(app)
        .post('/api/orders')
        .send({ items: [{ productId: 1, quantity: 1 }] })
        .expect(401);
    });
  });

  describe('GET /api/orders/:id', () => {
    it('returns order for owner', async () => {
      const createRes = await request(app)
        .post('/api/orders')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ items: [{ productId: product.id, quantity: 1 }] });

      const response = await request(app)
        .get(`/api/orders/${createRes.body.id}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body.id).toBe(createRes.body.id);
    });

    it('returns 404 for non-existent order', async () => {
      await request(app)
        .get('/api/orders/99999')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(404);
    });

    it('returns 403 for other user order', async () => {
      // Create order as first user
      const createRes = await request(app)
        .post('/api/orders')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ items: [{ productId: product.id, quantity: 1 }] });

      // Create second user
      const otherUser = await createUser({ email: 'other@example.com' });
      const otherLoginRes = await request(app)
        .post('/api/auth/login')
        .send({ email: 'other@example.com', password: 'password123' });

      // Try to access first user's order
      await request(app)
        .get(`/api/orders/${createRes.body.id}`)
        .set('Authorization', `Bearer ${otherLoginRes.body.token}`)
        .expect(403);
    });
  });

  describe('PATCH /api/orders/:id/status', () => {
    it('updates order status', async () => {
      const createRes = await request(app)
        .post('/api/orders')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ items: [{ productId: product.id, quantity: 1 }] });

      const response = await request(app)
        .patch(`/api/orders/${createRes.body.id}/status`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ status: 'paid' })
        .expect(200);

      expect(response.body.status).toBe('paid');
    });

    it('validates status transitions', async () => {
      const createRes = await request(app)
        .post('/api/orders')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ items: [{ productId: product.id, quantity: 1 }] });

      // Cannot go from pending to delivered
      await request(app)
        .patch(`/api/orders/${createRes.body.id}/status`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ status: 'delivered' })
        .expect(400);
    });
  });
});

Zobacz też


Napisane przez zespół Flipcards, na podstawie doświadczeń z rozmów rekrutacyjnych w firmach technologicznych i software house'ach w Polsce i za granicą.

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.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.