Тестирование и отладка Node-приложений в Docker-контейнерах

тестирование и отладка в контейнерах

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

Создание простого приложения

Примечание В качестве примера рассматривается простое Node-приложение. Представленные здесь принципы могут быть легко адаптированы к другим языкам программирования, например, Python, Go или .NET Core.

Создаем директорию проекта и в терминале перемещаемся в нее. Выполняем команду:

npm init

На вопрос о точке входа (entry point) вводим server.js и на вопрос о тестовой команде (test command) отвечаем jasmine-node spec . Остальные вопросы можно пропустить, оставив значение по умолчанию. По завершении работы этой команды будет создан файл package.json, который должен выглядеть примерно так:

{
  "name": "debug-node",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "jasmine-node spec",
    "start": "node server.js"
  },
  "author": "Gabriel Schenker",
  "license": "ISC",
  "dependencies": {
  }
}

Наше приложение будет использовать Express JS. Установим пакет командой:

npm install express --save

Теперь создаем файл server.js, который будет содержать следующий код:

var express = require('express');
var app = express();
var port = 3000;
var primes = require('./primes.js');

app.get('/', function(req, res){
    res.status(200).send('Testing and Debugging Sample');
})

app.get('/isPrime/:number', function(req, res){
    res.status(200).send(primes.isPrime(req.params.number));    
})

exports.stop = function(){
    server.close();
}
var server = app.listen(port, function(){
  console.log("Express server listening on port %d in %s mode", port, app.settings.env);
});

В строке 4 мы импортируем модуль primes.js, который содержит логику, позволяющую определить, является ли данное число простым. Таким образом, нужно добавить в проект файл primes.js со следующим содержимым:

exports.isPrime = function(number){
    for(var i = 2; i < number/2; i++) {
        if(number % i === 0) {
            return false;
        }
    }
    return number > 1;
}

Добавляем Dockerfile

Чтобы иметь возможность упаковать наше приложение в Docker-контейнер, нам нужно добавить Dockerfile в проект. Содержимое этого файла должно выглядеть так:

FROM node:4.2.3
# для тестирования
RUN npm install -g jasmine-node
RUN mkdir /app
WORKDIR /app
COPY package.json /app/
RUN npm install
COPY . /app
EXPOSE 3000 5858
ENTRYPOINT ["npm", "start"]

Обратите внимание, что в строке 3 мы устанавливаем jasmine-node, который нужен для запуска тестов. Также мы открываем не только порт 3000, но и порт 5858. Последний будет использоваться для присоединения отладчика. Также обратите внимание на то, что мы сначала копируем package.json в образ и запускаем npm install, и только после этого копируем оставшуюся часть папки приложения. Это помогает нам оптимизировать сборку Docker-образа.

Запуск приложения в контейнере

Теперь давайте соберем Docker-образ нашего приложения. Выполним команду:

docker build -t my-app .

Примечание Не забудьте указать точку в конце команды.

Теперь мы можем получить контейнер из этого образа:

docker run -d --name my-app -p 3000:3000 my-app

Мы должны увидеть длинный ID (хеш-код), который выводится в терминал. Чтобы повторно убедиться, работает ли контейнер, мы можем использовать команду docker ps, и должны увидеть следующее:

Если по какой-либо причине контейнер не создался, мы можем просмотреть логи создания командой docker logs my-app:

 Добавляем тесты

Для тестирования мы будем использовать Jasmine. Мы можем поставить его глобально на нашей машине, но зачем это делать, если у нас есть контейнер? В данной статье продемонстрирована глобальная установка Jasmine, но вы можете самостоятельно установить Jasmine в контейнер.  Давайте установим:

npm install -g jasmine-node

Теперь настроим несложный тест. Создайте папку spec и добавьте в нее файл primes-spec.js со следующим содержимым:

var sut = require('../primes.js');
describe("when evaluating if a given number is a prime", function(){
  describe("when the number is a prime", function(){
    it("should return 'true'", function(){
      expect(sut.isPrime(11)).toBe(true);
    });
  });
});

Здесь не будет описана логика Jasmine. Вы можете обратиться к подробной документации, чтобы разобраться с этим.

Как только мы определили тест, выполним эту команду в терминале:

npm test

Эта команда вызывает Jasmine-node, который мы определили в package.json. Вывод будет примерно такой:

Также мы можем добавлять новые тесты в приложение. Для этого нам нужно добавить модуль require:

npm install request --save

Теперь добавьте файл с именем server-spec.js в папку spec. Он должен иметь такое содержание:

var request = require("request");
var server = require("../server.js");
var base_url = "http://localhost:3000/";

describe("when testing projects endpoint", function(){
    it("should return status OK", function(done){
        request.get(base_url+"isPrime/11", function(error, response, body) {
            expect(response.statusCode).toBe(200);
            done();
        });
    });
    it("should return list of projects", function(done){
        request.get(base_url+'isPrime/11', function(error, response, body) {
            expect(JSON.parse(response.body)).toBe(true);
            done();
        })
    });
    
    // Остановка сервера
    it('should clean up', function(){
        server.stop();
    })
})

Теперь мы можем снова запустить все тесты: npm test.

Давайте запустим тест в контейнере. Для этого используется docker-compose. Добавим файл docker-compose.test.yml в проект:

sut:
  build: .
  ports:
    - "3000:3000"
  entrypoint: jasmine-node spec

Этот yaml-файл содержит инструкции по созданию образа контейнера с использованием Dockerfile, открытии порта 3000 и сопоставлении его с портом 3000 на хосте. Наконец, переопределяем значение entrypoint. Теперь в терминале введите следующую команду:

docker-compose -f docker-compose.test.yml up --build

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

Здесь создается Docker-контейнер и выполняются все тесты. Тесты, которые прошли успешно, возвращают код 0. Мы можем остановить тестирование командой:

docker-compose -f docker-compose.test.yml down

Отладка приложения

Мы научились выполнять автоматические тесты приложений. Но что насчет отладки? Ниже будет показано, как проходить код по строкам и проверять значения переменных.

Примечание В данном примере используется Visual Studio Code. Но эти действия можно совершить в большинстве современных редакторов.

Для начала нам нужно узнать, на каком IP-адресе находится хост Docker. Выполним команду:

docker-machine ip default

Добавим в проект файл docker-compose.debug.yml со следующим содержимым:

sut:
  build: .
  ports:
    - "3000:3000"
    - "5858:5858"
  entrypoint: node --debug=5858 server.js

Подобно файлу docker-compose для тестирования, мы используем Dockerfile для создания образа, открываем порт 3000 и порт 5858, сопоставляем их с теми же портами на хосте. Наконец, мы переопределяем точку входа node --debug = 5858 server.js. То есть запускаем узел в режиме отладки, прослушивая порт 5858. Мы можем начать сеанс отладки, используя эту команду:

docker-compose -f docker-compose.debug.yml up --build -d

В качестве последнего шага нам необходимо настроить Visual Studio Code для присоединения к Node-приложению, запущенному в контейнере. Добавим папку .vscode в наш проект. Внутри этой папки мы добавляем файл launch.json.

Содержимое файла должно быть следующим:

{
	"version": "0.1.0",
	// Список настроек.
	"configurations": [
		{
			// Имя параметра.
			"name": "Launch server.js",
			// Тип Конфигурации.
			"type": "node",
			// Путь к приложению.
			"program": "server.js",
			// Автоматическая остановка программы.
			"stopOnEntry": false,
			// Аргументы командной строки.
			"args": [],
			// Путь к директории проекта.
			"cwd": ".",
			"runtimeExecutable": null,
			"runtimeArgs": ["--nolazy"],
			// Переменные среды используемые в проекте.
			"env": {
				"NODE_ENV": "development"
			},
			"sourceMaps": false,
			"outDir": null
		},
		{
			"name": "Attach",
			"type": "node",
			"request": "attach",
			"port": 5858,
			"address": "192.168.99.100",
			"restart": false,
			"sourceMaps": false,
			"outDir": null,
			"localRoot": "${workspaceRoot}/",
			"remoteRoot": "/app/"
		}
	]
}

Обратите внимание на раздел с именем Attach. Вы должны убедиться, что в поле address командой docker-machine ip default указан IP-адрес вашего Docker-хоста.
Как только была добавлена конфигурация запуска, мы можем щелкнуть по кнопке отладки. Убедитесь, что выбрана настройка Attach, а затем нажмите кнопку запуска.

Теперь добавьте точку останова в строку 7 файла server.js. Откройте браузер и перейдите к 192.168.99.100:3000 (замените IP-адрес своим, если у вас другой). Отладчик должен остановиться в строке 7:

В окне отладки мы видим всю отладочную информацию:

Итог

Мы научились тестировать и дебажить приложения внутри Docker-контейнера. Преимущество этого метода в том, что нам не нужно загрязнять компьютер библиотеками и фреймворками для поддержки тестирования и отладки.

Перевод статьи «Testing and Debugging a Containerized Node application»