Рубрики
Uncategorized

Тестирование взаимодействия баз данных с шумом

Тестирование баз данных взаимодействия с шумом шумами быстро стало одним из самых популярных JavaScript T … Теги с базой данных, DEVOPS, JavaScript, Whest.

Тестирование взаимодействия баз данных с шумом

Jest Быстро стал одним из самых популярных библиотек тестирования JavaScript. Хотя шума может быть в основном используется в контексте приложений Frontend, в walrus.ai Мы используем jest для тестирования наших сервисов Backend Node.js.

Jest стремится сделать тестирование «восхитительными», а большой компонент этого восторга исходит от скорости. Например, по умолчанию работает одновременно с рабочими процессами, шаблон, который поощряет и даже требует тестируемой изоляции. Хотя это относительно простое для выполнения кода FrontEnd, есть общий соревновательный слон состояния в комнате для Code Backend: база данных.

Почему тестовые взаимодействия баз данных?

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

async function changeUserName(db, userId, username) {
  const userRepository = db.getRepository(User);
  const updated = await userRepository.updateUserName(userId, username);

  return updated;
}

Эта функция принимает дескриптор для соединения базы данных, пользователь пользователя и новое имя пользователя и обновляет имя пользователя в базе данных. Мы абстрагием в базовом SQL необходимо сделать обновление базы данных с Шаблон репозитория Отказ Это позволяет нам проверить эту функцию довольно легко.

describe('changeUserName', () => {
  it('should update username in db', async () => {
    const db = { getRepository: jest.fn() };
    const repository = { updateUserName: jest.fn() };

    db.getRepository.mockReturnValue(repository);
    repository.updateUserName.mockReturnValue(Promise.resolve('updated'));

    const result = await changeUserName(db, '1', 'username');

    expect(result).toEqual('updated');
    expect(repository.updateUserName).toHaveBeenCalledTimes(1);
    expect(repository.updateUserName).toHaveBeenCalledWith('1', 'username');
  });
});

Однако, что, если мы хотим проверить наш фактический репозиторий? Код для репозитория, вероятно, выглядит что-то вроде этого:

class UserRepository {
  ...
  public async update(id, username) {
      await this.db.sql(`
        UPDATE users SET username = :username WHERE id = :id
    `, { id, username });
  }
}

Если мы хотели проверить метод, мы, очевидно, мы можем издеваться от соединения БД и утверждать, что .sql. называется с ожидаемыми параметрами. Но что, если этот SQL недействителен или, вероятно, что если SQL действителен, но делает неправильную вещь?

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

Настройка базы данных для шума

Как мы уже обсуждали, шума по умолчанию выполняет тесты одновременно — это делает совместную базу данных проблематичной. Испытания, работающие параллельно, будут Clobber состояния базы данных друг друга, вызывая ложные сбои и хлопья.

Самый простой вариант преодолеть это ограничение — это запустить шутку с --runinband вариант. Это заставляет использовать только один процесс, чтобы управлять всеми вашими тестами. Однако это, вероятно, сделает ваш тестовый набор гораздо медленнее. На walrus.ai Это пробило наш тестовый набор с 10 до секунды до нескольких минут, и просто не было наполнено для наших процессов CI/CD постоянных развертываний.

К счастью, параллельное тестирование с базой данных — довольно решенная проблема. Мы можем просто раскрутить базу данных для каждого работника, которую мы используем. Если все тесты, работающие против конкретной базы данных, выполняются серийно, то нам не нужно беспокоиться о параллельных процессах, мутационных данных базы данных.

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

beforeWorker(async () => {
  db = await createDatabase(`db_${process.env.JEST_WORKER_ID}`);
});

// All tests run serially here.

afterWorker(async () => {
  await destroyDatabase(db);
});

К сожалению, в то время как шутка открывает Jest_worker_id Переменная среды для различения рабочих, Он не подвергает простое способы подключения в соответствии с рабочими методами настроек и методам разрыва Отказ

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

Во-первых, нам понадобится сценарий настройки теста, который готовит наши несколько баз данных.

Примечание. Следующие примеры кода используют Типрм С Но код может быть легко расширен для любой другой библиотеки взаимодействия базы данных, таких как Sequelize, Massive.js, Knex и т. Д.

(async () => {
  const connection = await createConnection({
    type: 'postgres',
    username: process.env.DATABASE_USER,
    password: process.env.DATABASE_PASSWORD,
    database: process.env.DATABASE_MASTER',
    host: process.env.DATABASE_HOST,
    port: 5432,
  });
  const databaseName = `walrus_test_template`;
  const workers = parseInt(process.env.JEST_WORKERS || '1');

  await connection.query(`DROP DATABASE IF EXISTS ${databaseName}`);
  await connection.query(`CREATE DATABASE ${databaseName}`);

  const templateDBConnection = await createConnection({
    name: 'templateConnection',
    type: 'postgres',
    username: process.env.DATABASE_USER,
    password: process.env.DATABASE_PASSWORD,
    database: 'walrus_test_template',
    host: process.env.DATABASE_HOST,
    migrations: ['src/migrations/*.ts'],
    port: 5432,
  });

  await templateDBConnection.runMigrations();
  await templateDBConnection.close();

  for (let i = 1; i <= workers; i++) {
    const workerDatabaseName = `walrus_test_${i}`;

    await connection.query(`DROP DATABASE IF EXISTS ${workerDatabaseName};`);
    await connection.query(`CREATE DATABASE ${workerDatabaseName} TEMPLATE ${databaseName};`);
  }

  await connection.close();
})();

С помощью этого скрипта мы создаем соединение с нашей базой данных, Postgres в этом случае. Затем, для каждого работника, который мы используем (устанавливаем статически в Meet jest_workers Переменную среды, мы инициализируем новую базу данных.

Поскольку мы используем Postgres, мы можем использовать удобную функцию, называемую базы данных шаблонов. Это делает новое создание базы данных дешево и позволяет нам только запускать наши миграции один раз. На высоком уровне мы создаем одну базу данных шаблонов, выполняйте наши миграции один раз против базы данных шаблонов, а затем быстро скопируйте в базу данных для каждого работника Jest.

Теперь нам просто нужно подключиться к правильной базе данных во всех наших тестах. С Jest_worker_id Переменная среды, это тривиальна:

beforeAll(async () => {
  connection = await createConnection({
    type: 'postgres',
    host: process.env.DATABASE_HOST,
    port: 5432,
    username: process.env.DATABASE_USER,
    password: process.env.DATABASE_PASSWORD,
    database: `walrus_test_${process.env.JEST_WORKER_ID}`,
    logging: false,
    entities,
    namingStrategy: new SnakeNamingStrategy(),
  });
});

afterAll(async () => {
  connection.close();
});

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

Уборка между тестами

Хотя параллельные тесты больше не проблематичны, у нас все еще есть проблема с последовательными тестами. Рассмотрим следующий пример, опять на наших надуманных Утернососиозиологии :

describe('UserRepository', () => {
    it('should create user', () => {
    await repository.createUser('username');

    expect(await repository.countUsers()).toEqual(1); 
  });

    it('should delete user', () => {
    const userId = await repository.createUser('username');
    await repository.deleteUser(userId);

    expect(await repository.countUsers()).toEqual(0); 
  });
});

Обратите внимание на что-то не так здесь? Второй тест потерпит неудачу. Хотя мы создаем различные базы данных для каждого из наших параллельных рабочих, тесты в одном и тем же файле запускаются последовательно. Это означает, что пользователь, созданный в первом тесте, все еще присутствует во втором тесте, вызывая провал теста.

Мы рассмотрели два подхода к решению этой проблемы. Первый подход — обернуть каждый тест в транзакции базы данных:

beforeEach(() => {
  db.startTransaction();
});

afterEach(() => {
  db.rollbackTransaction();
});

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

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

Другой, проще, но более медленный подход, чтобы просто очистить базу данных после каждого теста. Хотя это может быть медленнее, реже укусит нас позже. Мы можем сделать это в простом блоке Rebedeach.

beforeEach(async () => {
    const queryRunner = getConnection().createQueryRunner();

    await queryRunner.query(`
      DO
      $func$
      BEGIN
        EXECUTE (
          SELECT 'TRUNCATE TABLE ' || string_agg(oid::regclass::text, ', ') || ' CASCADE'
            FROM pg_class
            WHERE relkind = 'r'
            AND relnamespace = 'public'::regnamespace
        );
      END
      $func$;
    `);
    await queryRunner.release();
  });

Приведенный выше код илетет все наши таблицы и очищает их с помощью SQL Урезать команда. В walrus.ai Тестовый набор, это происходит в порядке Milliseconds и является цельнодоступным компромиссом для сохранения наших тестов.

Вывод

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

При тестировании базы данных взаимодействия базы данных с помощью Jest помогает увеличить охват тестирования единицы и интеграции, не жертвуя устойчивостью к тестированию, выполняя истинные сквозные тесты с помощью инструментов автоматизации браузера, такими как Selenium или Cypress, могут быть полными и нестабильными.

Целью «воплощение» состоит в том, чтобы сделать единицу и тестирование интеграции «восхитительную» — нашу цель на walrus.ai это сделать то же самое для сквозного тестирования. Инженерные команды могут писать тесты на простом английском языке, и мы заботимся о автоматизации тестов, разрешающих хлопьям, а также поддерживать тесты в качестве изменений их приложений. Наш приведенный выше пример выше показал, как проверить конец базы данных обновления имени пользователя, вот как просто соответствующий конечный тест с Walrus.ai может быть:

walrus -u your-application.com -i \
  'Login' \
  'Change your username' \
  'Verify you receive a confirmation email at the new email address' \
  'Verify your username is changed'

Оригинал: «https://dev.to/walrusai/testing-database-interactions-with-jest-519n»