JS Tower
Node.jsМодуль 7: Тестирование и деплой

Интеграционное тестирование

Supertest, тестирование API

Цель урока

В этом уроке ты научишься:

  • Тестировать API endpoints
  • Использовать Supertest
  • Настраивать тестовую базу данных

Supertest

Установка

npm install supertest --save-dev

Базовое использование

const request = require('supertest');
const app = require('../app');

describe('GET /api/users', () => {
  test('returns list of users', async () => {
    const response = await request(app)
      .get('/api/users')
      .expect('Content-Type', /json/)
      .expect(200);
    
    expect(response.body).toBeInstanceOf(Array);
  });
});

Структура приложения для тестов

// app.js
const express = require('express');
const app = express();

app.use(express.json());
app.use('/api/users', require('./routes/users'));

module.exports = app;

// server.js
const app = require('./app');

app.listen(3000, () => {
  console.log('Server running');
});

Тестирование CRUD

const request = require('supertest');
const app = require('../app');
const mongoose = require('mongoose');
const User = require('../models/User');

describe('Users API', () => {
  beforeAll(async () => {
    await mongoose.connect(process.env.TEST_DATABASE_URL);
  });
  
  afterAll(async () => {
    await mongoose.connection.close();
  });
  
  beforeEach(async () => {
    await User.deleteMany({});
  });
  
  describe('GET /api/users', () => {
    test('returns empty array when no users', async () => {
      const res = await request(app)
        .get('/api/users')
        .expect(200);
      
      expect(res.body.data).toEqual([]);
    });
    
    test('returns users', async () => {
      await User.create({ name: 'Test', email: 'test@test.com' });
      
      const res = await request(app)
        .get('/api/users')
        .expect(200);
      
      expect(res.body.data).toHaveLength(1);
    });
  });
  
  describe('POST /api/users', () => {
    test('creates user', async () => {
      const res = await request(app)
        .post('/api/users')
        .send({ name: 'Test', email: 'test@test.com' })
        .expect(201);
      
      expect(res.body.data.name).toBe('Test');
      
      const user = await User.findById(res.body.data.id);
      expect(user).toBeTruthy();
    });
    
    test('returns 400 for invalid data', async () => {
      const res = await request(app)
        .post('/api/users')
        .send({ name: '' })
        .expect(400);
      
      expect(res.body.error).toBeDefined();
    });
  });
  
  describe('PUT /api/users/:id', () => {
    test('updates user', async () => {
      const user = await User.create({ name: 'Old', email: 'test@test.com' });
      
      const res = await request(app)
        .put(`/api/users/${user._id}`)
        .send({ name: 'New' })
        .expect(200);
      
      expect(res.body.data.name).toBe('New');
    });
    
    test('returns 404 for non-existent user', async () => {
      const fakeId = new mongoose.Types.ObjectId();
      
      await request(app)
        .put(`/api/users/${fakeId}`)
        .send({ name: 'New' })
        .expect(404);
    });
  });
  
  describe('DELETE /api/users/:id', () => {
    test('deletes user', async () => {
      const user = await User.create({ name: 'Test', email: 'test@test.com' });
      
      await request(app)
        .delete(`/api/users/${user._id}`)
        .expect(204);
      
      const deleted = await User.findById(user._id);
      expect(deleted).toBeNull();
    });
  });
});

Тестирование с аутентификацией

const jwt = require('jsonwebtoken');

describe('Protected routes', () => {
  let token;
  let user;
  
  beforeEach(async () => {
    user = await User.create({
      name: 'Test',
      email: 'test@test.com',
      password: 'hashedpassword'
    });
    
    token = jwt.sign(
      { id: user._id },
      process.env.JWT_SECRET
    );
  });
  
  test('returns 401 without token', async () => {
    await request(app)
      .get('/api/profile')
      .expect(401);
  });
  
  test('returns profile with valid token', async () => {
    const res = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${token}`)
      .expect(200);
    
    expect(res.body.email).toBe('test@test.com');
  });
});

Тестовая база данных

MongoDB In-Memory

npm install mongodb-memory-server --save-dev
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');

let mongoServer;

beforeAll(async () => {
  mongoServer = await MongoMemoryServer.create();
  await mongoose.connect(mongoServer.getUri());
});

afterAll(async () => {
  await mongoose.disconnect();
  await mongoServer.stop();
});

Хелперы для тестов

// tests/helpers.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

async function createTestUser(data = {}) {
  return User.create({
    name: 'Test User',
    email: `test-${Date.now()}@test.com`,
    password: 'password123',
    ...data
  });
}

function generateToken(user) {
  return jwt.sign({ id: user._id }, process.env.JWT_SECRET);
}

async function authenticatedRequest(app, user) {
  const token = generateToken(user);
  return {
    get: (url) => request(app).get(url).set('Authorization', `Bearer ${token}`),
    post: (url) => request(app).post(url).set('Authorization', `Bearer ${token}`),
    put: (url) => request(app).put(url).set('Authorization', `Bearer ${token}`),
    delete: (url) => request(app).delete(url).set('Authorization', `Bearer ${token}`)
  };
}

module.exports = { createTestUser, generateToken, authenticatedRequest };

Практика

Задание: Тест регистрации

Задача: Напиши тест для endpoint регистрации.

Решение:

describe('POST /api/auth/register', () => {
  test('registers new user', async () => {
    const res = await request(app)
      .post('/api/auth/register')
      .send({
        name: 'New User',
        email: 'new@test.com',
        password: 'password123'
      })
      .expect(201);
    
    expect(res.body.token).toBeDefined();
    expect(res.body.user.email).toBe('new@test.com');
  });
  
  test('returns 400 if email exists', async () => {
    await User.create({ email: 'exists@test.com', password: 'pass' });
    
    const res = await request(app)
      .post('/api/auth/register')
      .send({ email: 'exists@test.com', password: 'pass' })
      .expect(400);
    
    expect(res.body.error).toContain('exists');
  });
});

Проверь себя

  1. Чем интеграционные тесты отличаются от юнит?
  2. Как тестировать защищённые маршруты?
  3. Зачем нужна тестовая база данных?