Рубрики
Uncategorized

API конец тестирования с Docker

Узнайте, как Docker может повысить вашу производительность для разработки и тестирования API. Шаг за шагом учебник. Tagged с Docker, CI, DevOps, WebDev.

Тестирование — это боль в целом. Некоторые не видят смысла. Некоторые видят это Но думайте об этом как о дополнительном шаге, замедляющем их. Иногда тесты есть, но очень долго бежать или нестабильно. В этой статье вы увидите, как вы сможете провести тесты для себя с помощью Docker.

Мы хотим быстрых, значимых и надежных тестов, написанных и поддерживаемых с минимальными усилиями. Это означает, что тесты, которые полезны для вас как разработчика на повседневной жизни. Они должны повысить вашу производительность и повысить качество вашего программного обеспечения. Наличие тестов, потому что все говорят, что «у вас должны быть тесты», не годится, если это замедляет вас.

Давайте посмотрим, как добиться этого с большими усилиями.

Пример, который мы собираемся проверить

В этой статье мы собираемся проверить API, построенный с узлом/экспрессом, и использовать Chai/Mocha для тестирования. Я выбрал стек js’y, потому что код очень короткий и легко читать. Применяемые принципы действительны для любого технического стека. Продолжайте читать, даже если JavaScript делает вас больным.

Пример будет охватывать простой набор конечных точек CRUD для пользователей. Этого более чем достаточно, чтобы понять концепцию и применить к более сложной бизнес -логике вашего API.

Мы собираемся использовать довольно стандартную среду для API:

  • База данных Postgres
  • Кластер Redis
  • Наш API будет использовать другие внешние API для выполнения своей работы

Вашему API может понадобиться другая среда. Принципы, применяемые в этой статье, останутся прежними. Вы используете разные изображения базовых Docker для запуска любого компонента, который вам может понадобиться.

Почему докер? А также На самом деле Docker сочиняется

Этот раздел представляет собой множество аргументов в пользу или использование Docker для тестирования. Вы можете пропустить его, если хотите сразу же добраться до технической части.

Болезненные альтернативы

Чтобы проверить ваш API в ближайшей к производственной среде, у вас есть два варианта. Вы можете издеваться над средой на уровне кода или запустить тесты на реальном сервере с установленной базой данных и т. Д.. Измешивание всего на уровне кода раскачивает код и конфигурацию нашего API. Также часто не очень репрезентативно, как API будет вести себя в производстве. Запуск вещи на реальном сервере — инфраструктура тяжелая. Это много настроек и обслуживания, и она не масштабируется. Имея общую базу данных, вы можете запускать только 1 тест за раз, чтобы гарантировать, что тестовые прогоны не мешают друг другу.

Docker Compose позволяет нам получить лучшее из обоих миров. Он создает «контейнерные» версии всех внешних частей, которые мы используем. Это насмешка, но снаружи нашего кода. Наш API думает, что это в реальной физической среде. Docker Compose также создаст изолированную сеть для всех контейнеров для данного тестового прогона. Это позволяет вам запустить несколько из них параллельно на вашем локальном компьютере или хосте CI.

Излишний?

Вы можете задаться вопросом, вообще ли это не излишне, чтобы закончить тестирование на конечное с помощью Docker Compose. Как насчет того, чтобы просто запустить модульные тесты вместо этого?

В течение последних 10 лет крупные монолитные приложения разделяются на более мелкие услуги (тенденция к гудным «микро службам»). Данный компонент API опирается на более внешние детали (инфраструктура или другие API). По мере того, как услуги становятся меньше, интеграция с инфраструктурой становится большей частью работы. Вы должны сохранить небольшой разрыв между вашей производством и средами разработки. В противном случае возникают проблемы при поиске производства. По определению эти проблемы появляются в самый худший момент. Они приведут к спешным исправлениям, падению качества и разочарованию для команды. Никто этого не хочет.

Вы можете задаться вопросом, запускаются ли конечные тесты с Docker Compose дольше, чем традиционные модульные тесты. Не совсем. В приведенном ниже примере мы увидим, что мы можем легко сохранить тесты менее 1 минуты. На больших преимуществах: тесты отражают поведение применения в реальном мире. Это более ценно, чем знать, работает ли ваш класс где -то в середине приложения или нет. Кроме того, если у вас сейчас нет тестов, начиная с конца до конца дает вам большие преимущества для небольших усилий. Вы будете знать все стеки приложения, работающие вместе для наиболее распространенных сценариев. Это уже что -то! Оттуда вы всегда можете усовершенствовать стратегию для модульных тестов критических частей вашего применения.

Наш первый тест

Давайте начнем с самой простой части. Наша API и база данных Postgres. И давайте запустим простой тест CRUD. Как только у нас появится эта структура, мы сможем добавить больше функций как к нашему компоненту, так и к проверке.

Вот наш минимальный API с GET/POST для создания и перечисления пользователей:

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

const config = require('./config');

const db = require('knex')({
  client: 'pg',
  connection: {
    host : config.db.host,
    user : config.db.user,
    password : config.db.password,
  },
});

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cors());

app.route('/api/users').post(async (req, res, next) => {
  try {
    const { email, firstname } = req.body;
    // ... validate inputs here ...
    const userData = { email, firstname };

    const result = await db('users').returning('id').insert(userData);
    const id = result[0];
    res.status(201).send({ id, ...userData });
  } catch (err) {
    console.log(`Error: Unable to create user: ${err.message}. ${err.stack}`);
    return next(err);
  }
});

app.route('/api/users').get((req, res, next) => {
  db('users')
  .select('id', 'email', 'firstname')
  .then(users => res.status(200).send(users))
  .catch(err => {
      console.log(`Unable to fetch users: ${err.message}. ${err.stack}`);
      return next(err);
  });
});

try {
  console.log("Starting web server...");

  const port = process.env.PORT || 8000;
  app.listen(port, () => console.log(`Server started on: ${port}`));
} catch(error) {
  console.error(error.stack);
}

Вот наши тесты, написанные с Chai. Тесты создают нового пользователя и принесут его обратно. Вы можете видеть, что тесты никоим образом не связаны с кодом нашего API. Server_url Переменная указывает конечную точку для тестирования. Это может быть местная или удаленная среда.

const chai = require("chai");
const chaiHttp = require("chai-http");
const should = chai.should();

const SERVER_URL = process.env.APP_URL || "http://localhost:8000";

chai.use(chaiHttp);

const TEST_USER = {
  email: "john@doe.com",
  firstname: "John"
};

let createdUserId;

describe("Users", () => {
  it("should create a new user", done => {
    chai
      .request(SERVER_URL)
      .post("/api/users")
      .send(TEST_USER)
      .end((err, res) => {
        if (err) done(err)
        res.should.have.status(201);
        res.should.be.json;
        res.body.should.be.a("object");
        res.body.should.have.property("id");
        done();
      });
  });

  it("should get the created user", done => {
    chai
      .request(SERVER_URL)
      .get("/api/users")
      .end((err, res) => {
        if (err) done(err)
        res.should.have.status(200);
        res.body.should.be.a("array");

        const user = res.body.pop();
        user.id.should.equal(createdUserId);
        user.email.should.equal(TEST_USER.email);
        user.firstname.should.equal(TEST_USER.firstname);
        done();
      });
  });
});

Хороший. Теперь, чтобы проверить наш API, давайте определим среду Docker Compose. Файл с именем Docker-compose.yml Опискует контейнеры, которые Docker должен работать.

version: '3.1'

services:
  db:
    image: postgres
    environment:
      POSTGRES_USER: john
      POSTGRES_PASSWORD: mysecretpassword
    expose:
      - 5432

  myapp:
    build: .
    image: myapp
    command: yarn start
    environment:
      APP_DB_HOST: db
      APP_DB_USER: john
      APP_DB_PASSWORD: mysecretpassword
    expose:
      - 8000
    depends_on:
      - db

  myapp-tests:
    image: myapp
    command: dockerize
        -wait tcp://db:5432 -wait tcp://myapp:8000 -timeout 10s
        bash -c "node db/init.js && yarn test"
    environment:
      APP_URL: http://myapp:8000
      APP_DB_HOST: db
      APP_DB_USER: john
      APP_DB_PASSWORD: mysecretpassword
    depends_on:
      - db
      - myapp

Итак, что у нас есть здесь. Есть 3 контейнера:

  • DB Выровняет новый экземпляр PostgreSQL. Мы используем общедоступное изображение Postgres из Docker Hub. Мы установили имя пользователя и пароль базы данных. Мы говорим Docker разоблачить порт 5432, что база данных будет прослушать, так что другие контейнеры может подключиться
  • myApp это контейнер, который запустит наш API. сборка Команда говорит Docker фактически построить изображение контейнера из нашего источника. Остальное похоже на контейнер DB: переменные среды и порты
  • MyApp-tests это контейнер, который выполнит наши тесты. Он будет использовать то же изображение, что и MyApp, потому что код уже будет там, поэтому нет необходимости создавать его снова. Команда Узел db/init.js && test yarn Запустите на контейнере инициализировать базу данных (создавать таблицы и т. Д.) И запустить тесты. Мы используем Dockerize, чтобы ждать, пока все требуемые серверы будут запущены. зависит_on Варианты гарантируют, что контейнеры начинаются в определенном порядке. Это не гарантирует, что база данных внутри контейнера DB фактически готова принять соединения. И что наш сервер API уже встал.

Определение среды похоже на 20 строк очень простых для понимания кода. Единственная мозговая часть — это определение окружающей среды. Имена пользователей, пароли и URL -адреса должны быть согласованными, поэтому контейнеры могут работать вместе.

Обратите внимание, что Docker Compose установит хост контейнеров, которые он создает на имя контейнера. Таким образом, база данных не будет доступна в рамках Localhost: 5432 но DB: 5432 . Точно так же, как наш API будет подан под MyApp: 8000 . Здесь нет никакого местного хода. Это означает, что ваш API должен поддерживать переменные среды, когда дело доходит до определения окружающей среды. Никаких жестких кодирующих вещей. Но это не имеет ничего общего с Docker или этой статьей. Настраиваемое приложение — точка 3 12 факторный манифест приложения , так что вы должны уже делать это.

Самое последнее, что нам нужно сказать Docker, это как на самом деле построить контейнер MyApp . Мы используем Dockerfile, как ниже. Контент специфичен для вашего технического стека, но идея состоит в том, чтобы объединить ваш API в запускаемом сервере.

Пример ниже для нашего узла API устанавливает Dockerize, устанавливает API Depencences и копирует код API внутри контейнера (сервер написан в RAW JS, поэтому не нужно компилировать его).

FROM node AS base

# Dockerize is needed to sync containers startup
ENV DOCKERIZE_VERSION v0.6.0
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
    && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
    && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz

RUN mkdir -p ~/app

WORKDIR ~/app

COPY package.json .
COPY yarn.lock .

FROM base AS dependencies

RUN yarn

FROM dependencies AS runtime

COPY . .

Обычно из строки Workdir ~/app А ниже вы запустили бы команды, которые создали бы ваше приложение.

И вот команда, которую мы используем для запуска тестов:

docker-compose up --build --abort-on-container-exit

Эта команда сообщит Docker Compose, чтобы раскрутить компоненты, определенные в нашем Docker-compose.yml файл. -build Флаг запустит сборку контейнера MyApp, выполнив содержание Dockerfile выше. -Abort-on-container-exit Позвольте Docker Compose, чтобы выключить среду, как только один контейнер выходит. Это работает хорошо, так как единственный компонент, предназначенный для выхода, — это тестовый контейнер MyApp-tests После выполнения тестов. Черри на торте, команда Docker-Compose выходит из того же кода выхода, что и контейнер, который вызвал выход. Это означает, что мы можем проверить, преуспели ли тесты или нет из командной строки. Это очень полезно для автоматических сборки в среде CI.

Разве это не идеальная настройка теста?

Полный пример — Здесь, на GitHub Анкет Вы можете клонировать репозиторий и запустить команду Docker Compose:

docker-compose up --build --abort-on-container-exit

Конечно, вам нужно установить Docker. У Docker возникает неприятная тенденция заставлять вас подписаться на учетную запись, чтобы загрузить эту вещь. Но тебе на самом деле не нужно. Перейдите в заметки о выпуске ( ссылка для Windows и Ссылка для Mac ) и загрузите не последнюю версию, а такую прямо раньше. Это прямая ссылка на загрузку.

Самый первый запуск тестов будет длиннее, чем обычно. Это потому, что Docker должен будет загрузить базовые изображения для ваших контейнеров и кэшировать несколько вещей. Следующие пробежки будут намного быстрее.

Журналы из пробега будут выглядеть ниже. Вы можете видеть, что Docker достаточно крут, чтобы положить журналы из всех компонентов на одну и ту же временную шкалу. Это очень удобно при поиске ошибок.

Creating tuto-api-e2e-testing_db_1    ... done
Creating tuto-api-e2e-testing_redis_1 ... done
Creating tuto-api-e2e-testing_myapp_1 ... done
Creating tuto-api-e2e-testing_myapp-tests_1 ... done
Attaching to tuto-api-e2e-testing_redis_1, tuto-api-e2e-testing_db_1, tuto-api-e2e-testing_myapp_1, tuto-api-e2e-testing_myapp-tests_1
db_1           | The files belonging to this database system will be owned by user "postgres".
redis_1        | 1:M 09 Nov 2019 21:57:22.161 * Running mode=standalone, port=6379.
myapp_1        | yarn run v1.19.0
redis_1        | 1:M 09 Nov 2019 21:57:22.162 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis_1        | 1:M 09 Nov 2019 21:57:22.162 # Server initialized
db_1           | This user must also own the server process.
db_1           | 
db_1           | The database cluster will be initialized with locale "en_US.utf8".
db_1           | The default database encoding has accordingly been set to "UTF8".
db_1           | The default text search configuration will be set to "english".
db_1           | 
db_1           | Data page checksums are disabled.
db_1           | 
db_1           | fixing permissions on existing directory /var/lib/postgresql/data ... ok
db_1           | creating subdirectories ... ok
db_1           | selecting dynamic shared memory implementation ... posix
myapp-tests_1  | 2019/11/09 21:57:25 Waiting for: tcp://db:5432
myapp-tests_1  | 2019/11/09 21:57:25 Waiting for: tcp://redis:6379
myapp-tests_1  | 2019/11/09 21:57:25 Waiting for: tcp://myapp:8000
myapp_1        | $ node server.js
redis_1        | 1:M 09 Nov 2019 21:57:22.163 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
db_1           | selecting default max_connections ... 100
myapp_1        | Starting web server...
myapp-tests_1  | 2019/11/09 21:57:25 Connected to tcp://myapp:8000
myapp-tests_1  | 2019/11/09 21:57:25 Connected to tcp://db:5432
redis_1        | 1:M 09 Nov 2019 21:57:22.164 * Ready to accept connections
myapp-tests_1  | 2019/11/09 21:57:25 Connected to tcp://redis:6379
myapp_1        | Server started on: 8000
db_1           | selecting default shared_buffers ... 128MB
db_1           | selecting default time zone ... Etc/UTC
db_1           | creating configuration files ... ok
db_1           | running bootstrap script ... ok
db_1           | performing post-bootstrap initialization ... ok
db_1           | syncing data to disk ... ok
db_1           | 
db_1           | 
db_1           | Success. You can now start the database server using:
db_1           | 
db_1           |     pg_ctl -D /var/lib/postgresql/data -l logfile start
db_1           | 
db_1           | initdb: warning: enabling "trust" authentication for local connections
db_1           | You can change this by editing pg_hba.conf or using the option -A, or
db_1           | --auth-local and --auth-host, the next time you run initdb.
db_1           | waiting for server to start....2019-11-09 21:57:24.328 UTC [41] LOG:  starting PostgreSQL 12.0 (Debian 12.0-2.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
db_1           | 2019-11-09 21:57:24.346 UTC [41] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db_1           | 2019-11-09 21:57:24.373 UTC [42] LOG:  database system was shut down at 2019-11-09 21:57:23 UTC
db_1           | 2019-11-09 21:57:24.383 UTC [41] LOG:  database system is ready to accept connections
db_1           |  done
db_1           | server started
db_1           | CREATE DATABASE
db_1           | 
db_1           | 
db_1           | /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
db_1           | 
db_1           | waiting for server to shut down....2019-11-09 21:57:24.907 UTC [41] LOG:  received fast shutdown request
db_1           | 2019-11-09 21:57:24.909 UTC [41] LOG:  aborting any active transactions
db_1           | 2019-11-09 21:57:24.914 UTC [41] LOG:  background worker "logical replication launcher" (PID 48) exited with exit code 1
db_1           | 2019-11-09 21:57:24.914 UTC [43] LOG:  shutting down
db_1           | 2019-11-09 21:57:24.930 UTC [41] LOG:  database system is shut down
db_1           |  done
db_1           | server stopped
db_1           | 
db_1           | PostgreSQL init process complete; ready for start up.
db_1           | 
db_1           | 2019-11-09 21:57:25.038 UTC [1] LOG:  starting PostgreSQL 12.0 (Debian 12.0-2.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
db_1           | 2019-11-09 21:57:25.039 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
db_1           | 2019-11-09 21:57:25.039 UTC [1] LOG:  listening on IPv6 address "::", port 5432
db_1           | 2019-11-09 21:57:25.052 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db_1           | 2019-11-09 21:57:25.071 UTC [59] LOG:  database system was shut down at 2019-11-09 21:57:24 UTC
db_1           | 2019-11-09 21:57:25.077 UTC [1] LOG:  database system is ready to accept connections
myapp-tests_1  | Creating tables ...
myapp-tests_1  | Creating table 'users'
myapp-tests_1  | Tables created succesfully
myapp-tests_1  | yarn run v1.19.0
myapp-tests_1  | $ mocha --timeout 10000 --bail
myapp-tests_1  | 
myapp-tests_1  | 
myapp-tests_1  |   Users
myapp-tests_1  | Mock server started on port: 8002
myapp-tests_1  |     ✓ should create a new user (151ms)
myapp-tests_1  |     ✓ should get the created user
myapp-tests_1  |     ✓ should not create user if mail is spammy
myapp-tests_1  |     ✓ should not create user if spammy mail API is down
myapp-tests_1  | 
myapp-tests_1  | 
myapp-tests_1  |   4 passing (234ms)
myapp-tests_1  | 
myapp-tests_1  | Done in 0.88s.
myapp-tests_1  | 2019/11/09 21:57:26 Command finished successfully.
tuto-api-e2e-testing_myapp-tests_1 exited with code 0

Мы можем видеть это DB это контейнер, который инициализирует самый длинный. Имеет смысл. Как только это сделано, тесты начинаются. Общее время выполнения на моем ноутбуке составляет 16 секунд. По сравнению с 880 мс, используемым для фактического выполнения тестов, это много. В практических тестах, которые проходят до 1 минуты, являются золотом, так как это почти немедленная обратная связь. 15 -х секунд накладные расходы — это покупка во времени, которая будет постоянной, поскольку вы добавляете больше тестов. Вы можете добавить сотни тестов и при этом сохранить время исполнения менее 1 минуты.

Вуаля! У нас есть нашу тестовую структуру. В реальном проекте следующим шагом будет улучшение функционального охвата вашего API с помощью большего количества тестов. Давайте рассмотрим охватываемые операции CRUD. Пришло время добавить больше элементов в нашу тестовую среду.

Добавление кластера Redis

Давайте добавим еще один элемент в нашу среду API, чтобы понять, что нужно. Осторожно, спойлеры: это не много.

Давайте представим, что наш API хранит пользовательские сеансы в кластере Redis. Если вы удивляетесь, почему мы это сделаем, представьте 100 случаев вашего API в производстве. Пользователи попадают в тот или иной сервер на основе балансировки нагрузки Roubin. Каждый запрос должен быть аутентифицирован. Это требует, чтобы данные профиля пользователя проверяли привилегии и другую бизнес -логику, специфичную для приложений. Один из них — совершить поездку в базу данных, чтобы извлечь данные каждый раз, когда вам это нужно, но это не очень эффективно. Использование кластера базы данных в памяти предоставляет данные доступными на всех серверах за стоимость чтения локальной переменной.

Вот как вы улучшаете свою тестовую среду Docker с помощью дополнительной услуги. Давайте добавим кластер Redis из официального изображения Docker (я сохранил только новые части файла):

services:
  db:
    ...

  redis:
    image: "redis:alpine"
    expose:
      - 6379

  myapp:
    environment:
      APP_REDIS_HOST: redis
      APP_REDIS_PORT: 6379
    ...
  myapp-tests:
    command: dockerize ... -wait tcp://redis:6379 ...
    environment:
      APP_REDIS_HOST: redis
      APP_REDIS_PORT: 6379
      ...
    ...

Вы можете видеть, что это не так много. Мы добавили новый контейнер под названием Redis Анкет Он использует официальное минимальное изображение Redis под названием Redis: альпийский . Мы добавили конфигурацию хоста Redis и порт в наш контейнер API. И мы сделали тесты ждать этого, а также другие контейнеры, прежде чем выполнять тесты.

Давайте изменим наше приложение, чтобы фактически использовать кластер Redis:

const redis = require('redis').createClient({
  host: config.redis.host,
  port: config.redis.port,
})

...

app.route('/api/users').post(async (req, res, next) => {
  try {
    const { email, firstname } = req.body;
    // ... validate inputs here ...
    const userData = { email, firstname };
    const result = await db('users').returning('id').insert(userData);
    const id = result[0];

    // Once the user is created store the data in the Redis cluster
    await redis.set(id, JSON.stringify(userData));

    res.status(201).send({ id, ...userData });
  } catch (err) {
    console.log(`Error: Unable to create user: ${err.message}. ${err.stack}`);
    return next(err);
  }
});

Давайте теперь изменим наши тесты, чтобы проверить, что кластер Redis заполняется правильными данными. Вот почему MyApp-tests Контейнер также получает конфигурацию хоста и порта Redis в Docker-compose.yml Анкет

it("should create a new user", done => {
  chai
    .request(SERVER_URL)
    .post("/api/users")
    .send(TEST_USER)
    .end((err, res) => {
      if (err) throw err;
      res.should.have.status(201);
      res.should.be.json;
      res.body.should.be.a("object");
      res.body.should.have.property("id");
      res.body.should.have.property("email");
      res.body.should.have.property("firstname");
      res.body.id.should.not.be.null;
      res.body.email.should.equal(TEST_USER.email);
      res.body.firstname.should.equal(TEST_USER.firstname);
      createdUserId = res.body.id;

      redis.get(createdUserId, (err, cacheData) => {
        if (err) throw err;
        cacheData = JSON.parse(cacheData);
        cacheData.should.have.property("email");
        cacheData.should.have.property("firstname");
        cacheData.email.should.equal(TEST_USER.email);
        cacheData.firstname.should.equal(TEST_USER.firstname);
        done();
      });
    });
});

Посмотрите, как это было легко. Вы можете создать сложную среду для ваших тестов, как вы собираете кирпичи Lego.

Мы можем увидеть еще одно преимущество такого рода тестирования на полную среду в контейнер. Тесты могут фактически изучать компоненты среды. Наши тесты могут не только проверить, что наш API возвращает правильные коды ответов и данные. Мы можем проверить, что данные в кластере Redis имеют правильные значения. Мы также могли бы проверить содержание базы данных.

Добавление API Mocks

Общим элементом для компонентов API является вызов других компонентов API.

Допустим, наше API необходимо проверить на наличие писем пользователями спамми при создании пользователя. Чек выполняется с использованием сторонней службы:

const validateUserEmail = async (email) => {
  const res = await fetch(`${config.app.externalUrl}/validate?email=${email}`);
  if(res.status !== 200) return false;
  const json = await res.json();
  return json.result === 'valid';
}

app.route('/api/users').post(async (req, res, next) => {
  try {
    const { email, firstname } = req.body;
    // ... validate inputs here ...
    const userData = { email, firstname };

    // We don't just create any user. Spammy emails should be rejected
    const isValidUser = await validateUserEmail(email);
    if(!isValidUser) {
      return res.sendStatus(403);
    }

    const result = await db('users').returning('id').insert(userData);
    const id = result[0];
    await redis.set(id, JSON.stringify(userData));
    res.status(201).send({ id, ...userData });
  } catch (err) {
    console.log(`Error: Unable to create user: ${err.message}. ${err.stack}`);
    return next(err);
  }
});

Теперь у нас есть проблема с тестированием чего -либо. Мы не можем создавать пользователей, если API для обнаружения спам -электронных писем недоступен. Изменение нашего API на обход этого шага в тестовом режиме является опасным загромождением кода.

Даже если бы мы могли использовать настоящий сторонний сервис, мы не хотим этого делать. Как правило, наши тесты не должны зависеть от внешней инфраструктуры. Прежде всего, потому что вы, вероятно, будете запускать свои тесты много как часть процесса CI. Для этой цели не так круто потреблять еще один производственный API. Во -вторых, API может быть временно снижен, провалив ваши тесты по неправильным причинам.

Правильное решение состоит в том, чтобы издеваться над внешними API в наших тестах.

Нет необходимости в каких -либо причудливых рамках. Мы построим общий макет в ванили JS в ~ 20 строках кода. Это даст нам возможность контролировать то, что API вернется к нашему компоненту. Это позволяет проверить сценарии ошибок.

Теперь давайте улучшим наши тесты.

const express = require("express");

...

const MOCK_SERVER_PORT = process.env.MOCK_SERVER_PORT || 8002;

// Some object to encapsulate attributes of our mock server
// The mock stores all requests it receives in the `requests` property.
const mock = {
  app: express(),
  server: null,
  requests: [],
  status: 404,
  responseBody: {}
};

// Define which response code and content the mock will be sending
const setupMock = (status, body) => {
  mock.status = status;
  mock.responseBody = body;
};

// Start the mock server
const initMock = async () => {
  mock.app.use(bodyParser.urlencoded({ extended: false }));
  mock.app.use(bodyParser.json());
  mock.app.use(cors());
  mock.app.get("*", (req, res) => {
    mock.requests.push(req);
    res.status(mock.status).send(mock.responseBody);
  });

  mock.server = await mock.app.listen(MOCK_SERVER_PORT);
  console.log(`Mock server started on port: ${MOCK_SERVER_PORT}`);
};

// Destroy the mock server
const teardownMock = () => {
  if (mock.server) {
    mock.server.close();
    delete mock.server;
  }
};

describe("Users", () => {
  // Our mock is started before any test starts ...
  before(async () => await initMock());

  // ... killed after all the tests are executed ...
  after(() => {
    redis.quit();
    teardownMock();
  });

  // ... and we reset the recorded requests between each test
  beforeEach(() => (mock.requests = []));

  it("should create a new user", done => {
    // The mock will tell us the email is valid in this test
    setupMock(200, { result: "valid" });

    chai
      .request(SERVER_URL)
      .post("/api/users")
      .send(TEST_USER)
      .end((err, res) => {
        // ... check response and redis as before
        createdUserId = res.body.id;

        // Verify that the API called the mocked service with the right parameters
        mock.requests.length.should.equal(1);
        mock.requests[0].path.should.equal("/api/validate");
        mock.requests[0].query.should.have.property("email");
        mock.requests[0].query.email.should.equal(TEST_USER.email);
        done();
      });
  });
});

В настоящее время тесты проверяют, что внешний API был получен с помощью соответствующих данных во время вызова нашего API.

Мы также можем добавить другие тесты, проверяющие, как ведет себя наш API на основе внешних кодов ответов API:

describe("Users", () => {
  it("should not create user if mail is spammy", done => {
    // The mock will tell us the email is NOT valid in this test ...
    setupMock(200, { result: "invalid" });

    chai
      .request(SERVER_URL)
      .post("/api/users")
      .send(TEST_USER)
      .end((err, res) => {
        // ... so the API should fail to create the user
        // We could test that the DB and Redis are empty here
        res.should.have.status(403);
        done();
      });
  });

  it("should not create user if spammy mail API is down", done => {
    // The mock will tell us the email checking service
    //  is down for this test ...
    setupMock(500, {});

    chai
      .request(SERVER_URL)
      .post("/api/users")
      .send(TEST_USER)
      .end((err, res) => {
        // ... in that case also a user should not be created
        res.should.have.status(403);
        done();
      });
  });
});

Как вы обрабатываете ошибки от сторонних API в вашем приложении, конечно, зависит от вас. Но вы поняли.

Чтобы запустить эти тесты, мы должны сообщить контейнер myApp Каков базовый URL сторонней службы:

  myapp:
    environment:
      APP_EXTERNAL_URL: http://myapp-tests:8002/api
    ...

  myapp-tests:
    environment:
      MOCK_SERVER_PORT: 8002
    ...

Заключение и несколько других мыслей

Надеемся, что эта статья дала вам вкус того, что Docker Compose может сделать для вас, когда дело доходит до тестирования API. Полный пример — Здесь, на GitHub Анкет

Использование Docker Compose делает тесты быстро работать в среде, близкой к производству. Это не требует адаптации к коду вашего компонента. Единственным требованием является поддержка конфигурации переменных среды.

Логика компонентов в этом примере очень проста, но принципы применимы к любому API. Ваши тесты будут просто длиннее или более сложными. Они также относятся к любому технологическому стеку, который можно помещать в контейнер (это все). И как только вы будете там, вы на шаг от развертывания ваших контейнеров в производство, если это необходимо.

Если у вас сейчас нет тестов, это то, как я рекомендую вам начать. Конец к конечному тестированию с Docker Compose. Это настолько просто, что вы можете пройти первый тест через несколько часов. Не стесняйтесь Обратитесь ко мне Если у вас есть вопросы или нужен совет. Я был бы рад помочь.

Я надеюсь, что вам понравилась эта статья, и вы начнете тестировать ваши API с Docker Compose. После того, как у вас будут готовы тесты, вы можете запустить их из коробки на нашей платформе непрерывной интеграции Fire Ci Анкет

Последняя идея для успеха с автоматическим тестированием.

Когда дело доходит до поддержания больших тестовых люксов, наиболее важной особенностью является то, что тесты легко читаются и понимают. Это является ключом к мотивации вашей команды, чтобы держать тесты в курсе. Сложные тестовые рамки вряд ли будут правильно использовать в долгосрочной перспективе. Независимо от стека для вашего API, вы можете рассмотреть возможность использования Chai/Mocha для написания тестов для него. Может показаться необычным иметь разные стеки для кода времени выполнения и тестового кода, но если она выполняет задание … Как вы можете видеть из примеров в этой статье, тестирование API REST с Chai/Mocha так же просто, насколько это возможно. Кривая обучения близка к нулю. Так что, если у вас вообще нет тестов, и у вас есть API отдыха для тестирования, написанного на Java, Python, ROR, .NET или любом другом стеке, вы можете попробовать Chai/Mocha.

Если вам интересно, как начать с непрерывной интеграции, я написал об этом более широкое руководство. Вот это: Как начать с непрерывной интеграции Анкет

Первоначально опубликовано на Блог Fire CI 6 ноября 2019 года.

Оригинал: «https://dev.to/jpdelimat/api-end-to-end-testing-with-docker-4a0b»