Интеграционное тестирование и модульное тестирование
Модульные тесты отлично подходят для тестирования функции на определенное поведение. Если вы правильно кодируете и правильно создаете свои «имитируемые» зависимости, вы можете быть достаточно уверены в поведении своего кода.
Но наш код не живет изолированно, и нам нужно убедиться, что все «части» связаны и работают вместе так, как мы ожидаем. Здесь на помощь приходят интеграционные тесты.
Хороший способ объяснить разницу заключается в том, что модульный тест проверяет, что значение (скажем, электронное письмо для простоты) проходит тест на бизнес-логику (возможно, регулярное выражение или что-то в этом роде - возможно, проверяет 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
- Создавайте потрясающие коды и тестируйте их
Надеюсь, вам понравилась эта статья, и мы всегда приветствуем предложения, комментарии, исправления и другие мемы.