Использование Jest для имитации Elasticsearch

Использование 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, не перезапуская весь набор:

Running jest tests using Webstorm

Ресурсы

Теперь вы знаете, как тестировать запросы Elasticsearch с помощью jest-elasticsearch

.

Кроме того, у вас есть небольшой план репозиторий с готовой настройкой.

Надеюсь, эта статья поможет вам настроить и протестировать эластичный проект!

Хотите подключиться?

Следите за мной в Твиттере!


Также опубликовано здесь


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE