Интеграционное тестирование и модульное тестирование

Модульные тесты отлично подходят для тестирования функции на определенное поведение. Если вы правильно кодируете и правильно создаете свои «имитируемые» зависимости, вы можете быть достаточно уверены в поведении своего кода.

Но наш код не живет изолированно, и нам нужно убедиться, что все «части» связаны и работают вместе так, как мы ожидаем. Здесь на помощь приходят интеграционные тесты.

Хороший способ объяснить разницу заключается в том, что модульный тест проверяет, что значение (скажем, электронное письмо для простоты) проходит тест на бизнес-логику (возможно, регулярное выражение или что-то в этом роде - возможно, проверяет URL-адрес), а также адрес электронной почты и правила. будут предоставлены как макеты / заглушки / жестко запрограммированные в тесте, в то время как интеграционный тест этого будет проверять ту же логику, но также извлекать правила и значение из базы данных - таким образом проверяя, что все части подходят друг к другу и работают.

Если вам нужно больше примеров или вы хотите узнать больше об этом, есть отличные ресурсы по Medium, а также по Stack Overflow и т. Д. Остальная часть этой статьи предполагает, что вы знакомы с NodeJS и тестируете его (здесь мы используем Mocha - но не стесняйтесь использовать все, что вам нравится).

Получение образа MS-SQL

Для начала вы захотите вытащить образ Docker, просто запустите команду docker pull microsoft/mssql-server-linux:2017-latest (Также, если вы не установили Docker, вы тоже можете это сделать 😃 )

Это может занять несколько минут в зависимости от того, что вы установили в кеше Docker.

После этого не забудьте щелкнуть правой кнопкой мыши, перейти к «Настройки…» и включить: «Показать демон на tcp: // localhost: 2375». Как мы увидим в нескольких разделах, для правильной работы Docker-модема необходимо установить значение process.env.DOCKER_HOST.

Отложить Mocha для настройки

Поскольку нам нужно несколько минут, чтобы развернуть контейнер и развернуть схему, мы будем использовать флаг --delay для Mocha.

Это добавляет глобальную функцию run(), которую необходимо вызвать после завершения настройки.

Вы также должны использовать флаг --exit, который убьет Mocha после тестового запуска, даже если сокет открыт.

Подготовка к запуску

В этом примере мы используем флаг --require, чтобы потребовать файл перед запуском теста. В этом файле используется IIFE (немедленно вызываемое выражение функции), потому что нам нужно вызвать некоторые асинхронные функции и дождаться их, а затем вызвать функцию done() сверху. Это можно сделать с помощью обратных вызовов, но это не так чисто.

IIFE должен выглядеть так:

(async () => {
    const container = require('./infra/container');
    await container.createAsync();
    await container.initializeDbAsync();
    run(); // this kicks off Mocha
    beforeEach(async () => {
        console.log('Clearing db!');
        await container.clearDatabaseAsync();
    });
    after(async () => {
        console.log('Deleting container!');
        await container.deleteAsync();
    });
})();

Раскрутка контейнера из узла

В приведенном выше IIFE у нас есть метод container.createAsync();, который отвечает за настройку контейнера.

const { Docker } = require('node-docker-api');
const docker = new Docker();
...
async function createAsync() {
    const container = await docker.container.create({
        Image: 'microsoft/mssql-server-linux:2017-latest',
        name: 'mssqltest',
        ExposedPorts: { '1433/tcp': {} },
        HostConfig: {
            PortBindings: {
                '1433/tcp': [{ HostPort: '<EXPOSED_PORT>' }]
            }
        },
        Env: ['SA_PASSWORD=<S00p3rS3cUr3>', 'ACCEPT_EULA=Y']
    });
    console.log('Container built.. starting..');
    await container.start();
    console.log('Container started... waiting for boot...');
    sqlContainer = container;
    await checkSqlBootedAsync();
    console.log('Container booted!');
}

Контейнер создается с помощью async метода docker.container.create, для экземпляра docker необходимо установить process.env.DOCKER_HOST, в нашем случае у нас запущен локальный сервер Docker (см. Извлечение образа MS-SQL), поэтому мы будем его использовать.

Опции поступают от модема dockerode и использует Docker API.

После того, как контейнер развернется, нам нужно убедиться, что SQL завершил работу, наш порт - ‹EXPOSED_PORT›, а пароль - ‹S00p3rS3cUr3› (это заполнители, поэтому сделайте убедитесь, что вы указали что-то действительное).

Если вы хотите узнать больше о том, что происходит здесь с опцией EULA и т. Д., Ознакомьтесь с руководством здесь от Microsoft.

Поскольку для загрузки SQL-сервера требуется несколько секунд, мы хотим убедиться, что он работает, прежде чем запускать набор тестов. Решение, которое мы здесь придумали, заключалось в том, чтобы постоянно пытаться подключаться в течение 15 секунд каждые 1/2 секунды, а когда оно подключается, выходить.

Если не удается подключиться в течение 15 секунд, что-то пошло не так, и мы должны продолжить расследование. Параметры masterDb.config должны соответствовать тому, где вы размещаете Docker и какой порт вы используете 1433 для хоста. Также запомните пароль, который вы установили для sa.

async function checkSqlBootedAsync() {
    const timeout = setTimeout(async () => {
        console.log('Was not able to connect to SQL container in 15000 ms. Exiting..');
        await deleteAndExitAsync();
    }, 15000);
    let connecting = true;
    const mssql = require('mssql');
    console.log('Attempting connection... ');
    while (connecting) {
        try {
            mssql.close();
// don't use await! It doesn't play nice with the loop 
            mssql.connect(masterDb.config).then(() => {
                clearTimeout(timeout);
                connecting = false;
            }).catch();
        }
        catch (e) {
            // sink
        }
        await sleep(500);
    }
    mssql.close();
}

Развертывание схемы БД с помощью Sequelize

Мы можем быстро использовать Sequelize для развертывания схемы с помощью функции sync, тогда, как мы увидим ниже, рекомендуется установить какой-то флаг, чтобы предотвратить стирание не тестовой БД.

Однако сначала мы хотим создать базу данных, используя главное соединение. Код будет выглядеть примерно так:

async function initializeDbAsync() {
    const sql = 'CREATE DATABASE [MySuperIntegrationTestDB];';
    await masterDb.queryAsync(sql, {});
    await sequelize.sync();
    return setTestingDbAsync();
}

Проверки безопасности

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

А если вы еще не побегали, купите лотерейный билет, потому что, черт возьми, вам повезло.

Это причина для создания инфраструктуры для резервного копирования и прочего, препятствия, если хотите, чтобы предотвратить человеческую ошибку. Хотя эта инфраструктура интеграционного тестирования, которую вы только что закончили настраивать, великолепна, есть вероятность, что вы неправильно настроили переменные среды и т. Д.

Я предложу здесь одно возможное решение, но не стесняйтесь использовать собственное (или предлагайте больше в комментариях!).

Здесь мы будем использовать таблицу SystemConfiguration и иметь пару "ключ-значение" для ключа TestDB, значение которой должно быть истинным для усечения таблиц. Также на нескольких этапах я рекомендую проверять NODE_ENV переменную среды на test, чтобы убедиться, что вы случайно не запустили этот код в не тестовой среде.

В конце последнего раздела мы увидели вызов setTestingDbAsync, содержимое которого выглядит следующим образом:

async function setTestingDbAsync() {
    const configSql =
        "INSERT INTO [SystemConfiguration] ([key], [value]) VALUES (?, '1')";
    return sequelize.query(configSql, {replacements: [systemConfigurations.TestDB]});
}

Это устанавливает значение в базе данных, которое мы проверим в следующем фрагменте. Вот фрагмент кода, который проверит наличие значения для ключа TestDB (предоставленного из файла consts), который мы только что установили.

const result = await SystemConfiguration.findOne({ where: {key: systemConfigurations.TestDB }});
    if (!result) {
        console.log('Not test environment, missing config key!!!!');
        // bail out and clean up here
    }
// otherwise continue

Протирание теста перед каждым запуском

Взяв приведенный выше код и комбинируя его с чем-то для очистки базы данных, мы получаем следующую функцию:

const useSql = 'USE [MySuperIntegrationTestDB];';
 
async function clearDatabaseAsync() {
    const result = await SystemConfiguration.findOne({ where: {key: systemConfigurations.TestDB }});
    if (!result || !result.value) {
        console.log('Not test environment, missing config key!!!!');
        await deleteAndExitAsync();
    }
    const clearSql = `${useSql}
       EXEC sp_MSForEachTable 'DISABLE TRIGGER ALL ON ?'
       EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'
       EXEC sp_MSForEachTable 'DELETE FROM ?'
       EXEC sp_MSForEachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL'
       EXEC sp_MSForEachTable 'ENABLE TRIGGER ALL ON ?'`;
    await sequelize.query(clearSql);
    return setTestingDbAsync();
}
async function setTestingDbAsync() {
    const configSql = "INSERT INTO [SystemConfiguration] ([key], [value]) VALUES (?, '1')";
    return sequelize.query(configSql, {replacements: [systemConfigurations.TestDB]});
}

Это проверит наличие значения ключа TestDB в таблице SystemConfiguration перед продолжением. Если его там нет, процесс будет закрыт.

Как это работает в контексте Mocha?

Если вы помните, в IIFE у нас был вызов beforeEach, это то место, где вы хотите установить этот хук, чтобы у вас была чистая база данных для каждого теста.

beforeEach(async () => {
        console.log('Clearing db!');
        await container.clearDatabaseAsync();
    });

Выключение / Разборка

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

После выглядит так:

after(async () => {
        console.log('Deleting container!');
        await container.deleteAsync();
    });

А код внутри container.deleteAsync(); выглядит так:

async function deleteAsync() {
    return sqlContainer.delete({ force: true });
}

Собираем все вместе

Поскольку эта статья была немного многословной и немного перескакивала, вот основные моменты того, что нужно сделать, чтобы это работало:

  • Отложите Mocha с помощью --delay
  • Требовать сценарий установки и использовать IIFE для настройки контейнера / БД.
  • Раскрутите экземпляр контейнера Docker, дождитесь загрузки SQL
  • Разверните схему с помощью Sequelize, а также поставьте проверку безопасности, чтобы мы не стирали неконтролируемую БД.
  • Подключите логику очистки к ловушке beforeEach
  • Подключите логику разрыва к крючку after
  • Создавайте потрясающие коды и тестируйте их

Надеюсь, вам понравилась эта статья, и мы всегда приветствуем предложения, комментарии, исправления и другие мемы.

Удачи и счастливого тестирования!