Использование Jest для имитации Elasticsearch
5 декабря 2022 г.Поскольку вы открыли эту статью, я предполагаю, что вы либо используете Elasticsearch, либо планируете это делать.
Еще одно предположение: вы определенно сочли отличной идеей заблокировать свою функциональность с помощью некоторых интеграционных тестов. И это действительно так!
Текущая компания, в которой я работаю, имеет более 90% покрытия кода как модульными, так и интеграционными тестами! Я бы порекомендовал всем покрыть свою кодовую базу тестами, поскольку, как сказал один мудрый человек:
<цитата>[Без модульных тестов] Вы не проводите рефакторинг, вы просто меняете дерьмо. — Гамлет Д'Арси
Настройка
Представьте, что у вас есть простой сервер RESTful с некоторой логикой, использующей Elasticsearch. В текущей витрине — CRUD сервер.
const Hapi = require('@hapi/hapi');
const Qs = require('qs');
const {createHandler} = require("./create/index.js");
const {readAllHandler, readHandler} = require("./read/index.js");
const {updateHandler} = require("./update/index.js");
const {deleteAllHandler, deleteHandler} = require("./delete/index.js");
const init = async () => {
const server = Hapi.server({
port: 3000,
host: 'localhost',
query: {
parser: (query) => Qs.parse(query)
}
});
server.route({
method: 'POST',
path: '/',
handler: createHandler
});
server.route({
method: 'GET',
path: '/{id}',
handler: readHandler
});
server.route({
method: 'GET',
path: '/',
handler: readAllHandler
});
server.route({
method: 'PATCH',
path: '/{id}',
handler: updateHandler
});
server.route({
method: 'DELETE',
path: '/{id}',
handler: deleteHandler
});
server.route({
method: 'DELETE',
path: '/',
handler: deleteAllHandler
});
await server.start();
server.events.on('log', (event, tags) => {
console.log({event}, {tags})
if (tags.error) {
console.log(`Server error: ${event.error ? event.error.message : 'unknown'}`);
}
});
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();
Войти в полноэкранный режим Выйти из полноэкранного режима. Теперь вам нужно покрыть логику каждого маршрута некоторыми тестами, чтобы заблокировать функциональность и предотвратить нарушение бизнес-логики.
Одно из очевидных, но не простых решений — использовать Docker и каждый раз запускать Elastic для тестов.
Однако стоит ли? Я имею в виду, действительно ли вы хотите увеличить время раскрутки конвейерной среды? Есть ли готовые решения для этого?
Этот подключаемый модуль загружает и кэширует двоичный файл Elasticsearch при запуске jest, затем подключаемый модуль автоматически запускает Elastic на определенных портах и отключает его после завершения тестов.
Как добавить тесты?
У меня есть пример настройки шутливых тестов и без них в pull request так что вы можете сравнить все изменения. А теперь давайте рассмотрим это шаг за шагом.
1. Установите дополнительные модули
yarn add --dev jest @shelf/jest-elasticsearch @types/jest
Войти в полноэкранный режим Выйти из полноэкранного режима
2. Добавьте jest-config.js
touch jest.config.js
Войти в полноэкранный режим Выйти из полноэкранного режима
module.exports = {
preset: '@shelf/jest-elasticsearch',
clearMocks: true,
collectCoverage: true,
coverageDirectory: "coverage",
coverageProvider: "v8"
};
Войти в полноэкранный режим Выйти из полноэкранного режима
Кроме того, вы можете создать конфигурацию jest самостоятельно, используя инструмент командной строки.
3. Добавьте jest-es-config.js для плагина
touch jest-es-config.js
Войти в полноэкранный режим Выйти из полноэкранного режима
const {index} = require('./src/elastic.js');
module.exports = () => {
return {
esVersion: '8.4.0',
clusterName: 'things-cluster',
nodeName: 'things-node',
port: 9200,
indexes: [
{
name: index,
body: {
settings: {
number_of_shards: '1',
number_of_replicas: '1'
},
mappings: {
dynamic: false,
properties: {
id: {
type: 'keyword'
},
value: {
type: 'integer'
},
type: {
type: 'keyword'
},
name: {
type: 'keyword'
},
}
}
}
}
]
};
};
Войти в полноэкранный режим Выйти из полноэкранного режима
4. Расширить скрипт package.json для запуска тестов
{
"scripts": {
"test": "jest"
"serve": "node src/index.js"
}
}
Войти в полноэкранный режим Выйти из полноэкранного режима
5. Настройте эластичный клиент
const dotenv = require('dotenv')
dotenv.config()
const {Client} = require('@elastic/elasticsearch');
module.exports.client = new Client({
node: process.env.NODE_ENV === 'test' ? 'http://localhost:9200' : process.env.ES_URL
})
module.exports.index = 'things'
Войти в полноэкранный режим Выйти из полноэкранного режима
Добавить условие, которое будет использовать NODE_ENV для подключения к локальному эластичному кабелю всякий раз, когда мы запускаем тесты.
Прибыль!
Теперь все готово для написания и запуска тестов. Все маршруты полностью покрыты и хранятся здесь: elastic-jest-example
В качестве примера рассмотрим создание бизнес-логики функции.
const {ulid} = require('ulid');
const {client, index} = require("../elastic.js");
module.exports.createHandler = async (request, h) => {
if (Object.keys(request.payload))
try {
const res = await this.create(request.payload)
return h.response(res).code(200);
} catch (e) {
console.log({e})
return h.response({e}).code(400);
}
}
// let's cover this function with some tests
module.exports.create = async (entity) => {
const {
type,
value,
name,
} = entity;
const document = {
id: ulid(),
type: type.trim().toLowerCase(),
value: +value.toFixed(0),
name: name.trim()
}
await client.index({
index,
document
});
return document.id
}
Войти в полноэкранный режим Выйти из полноэкранного режима
Создайте тестовый файл и добавьте пару операторов.
touch src/create/index.test.js
Войти в полноэкранный режим Выйти из полноэкранного режима
const {create} = require("./index.js");
const {client, index} = require("../elastic");
describe('#create', () => {
// clear elastic every time before running it the statement.
// It's really important since each test would be idempotent.
beforeEach(async () => {
await client.deleteByQuery({
index,
query: {
match_all: {}
}
})
await client.indices.refresh({index})
})
it('should insert data', async () => {
expect.assertions(3);
const res = await create({type: 'some', value: 100, name: 'jacket'})
await client.indices.refresh();
const data = await client.search({
index,
query: {
match: {
"id": res
}
}
})
expect(res).toEqual(expect.any(String))
expect(res).toHaveLength(26);
expect(data.hits.hits[0]._source).toEqual({
"id": res,
"name": "jacket",
"type": "some",
"value": 100
}
);
})
it('should insert and process the inserted fields', async () => {
const res = await create({type: 'UPPERCASE', value: 25.99, name: ' spaces '})
await client.indices.refresh();
const data = await client.search({
index,
query: {
match: {
"id": res
}
}
})
expect(data.hits.hits[0]._source).toEqual({
"id": res,
"name": "spaces",
"type": "uppercase",
"value": 26
}
);
})
});
Войти в полноэкранный режим Выйти из полноэкранного режима
Базовый поток тестирования для каждой функции бизнес-логики можно просто описать следующим образом:
вставить данные-> запустить проверенную функцию -> проверить выходные данные -> очистить данные -> повторить
Вставку/удаление данных можно улучшить, объединив их в вспомогательные функции и используя дополнительные библиотеки-халтурщики.
Эластичный разрыв управляется самой библиотекой @shelf/jest-elasticsearch.
Еще одно полезное соглашение – покрывать каждую тестируемую функцию блоком описания, чтобы впоследствии можно было легко запустить конкретный тест с помощью помощника IDE, не перезапуская весь набор:
Ресурсы
Теперь вы знаете, как тестировать запросы Elasticsearch с помощью jest-elasticsearch
.Кроме того, у вас есть небольшой план репозиторий с готовой настройкой.
Надеюсь, эта статья поможет вам настроить и протестировать эластичный проект!
Хотите подключиться?
Следите за мной в Твиттере!
Также опубликовано здесь
Оригинал
