Как обеспечить запуск файла Jest bootstrap первым? - PullRequest
0 голосов
/ 06 мая 2020

Введение

Я застрял с обещаниями базы данных, которые я использую внутри среды тестирования Jest. Все работает в неправильном порядке, и после некоторых из моих последних изменений Jest не завершает работу правильно, потому что неизвестная асинхронная операция не обрабатывается. Я новичок в Node / Jest.

Вот что я пытаюсь сделать. Я настраиваю Jest в среде с несколькими контейнерами Docker для вызова внутренних API-интерфейсов для тестирования их выходных данных JSON и для запуска служебных функций, чтобы увидеть, какие изменения они вносят в базу данных тестовой среды MySQL. Для этого я:

  • использую параметр конфигурации setupFilesAfterEnv Jest, чтобы указать на установочный файл, который, как я считаю, должен быть запущен первым
  • с использованием установочного файла для уничтожения тестовая база данных (если она существует), чтобы воссоздать ее, а затем создать несколько таблиц
  • с использованием mysql2/promise для выполнения операций с базой данных
  • с использованием beforeEach(() => {}) в тесте чтобы усечь все таблицы, чтобы вставить данные для каждого теста, чтобы тесты не зависели друг от друга

Я могу подтвердить, что установочный файл для Jest запускается до первого (и только ) тестовый файл, но что странно, так это то, что Promise catch() в тестовом файле появляется перед finally в установочном файле.

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

Код

Вот файл установки, красивый и простой:

// Little fix for Jest, see https://stackoverflow.com/a/54175600
require('mysql2/node_modules/iconv-lite').encodingExists('foo');

// Let's create a database/tables here
const mysql = require('mysql2/promise');
import TestDatabase from './TestDatabase';
var config = require('../config/config.json');

console.log('Here is the bootstrap');

const initDatabase = () => {
  let database = new TestDatabase(mysql, config);
  database.connect('test').then(() => {
    return database.dropDatabase('contributor_test');
  }).then(() => {
    return database.createDatabase('contributor_test');
  }).then(() => {
    return database.useDatabase('contributor_test');
  }).then(() => {
    return database.createTables();
  }).then(() => {
    return database.close();
  }).finally(() => {
    console.log('Finished once-off db setup');
  });
};
initDatabase();

config.json - это просто имена пользователей / пароли и не стоит показывать здесь.

Как вы n см. этот код использует класс служебной базы данных, а именно:

export default class TestDatabase {

  constructor(mysql, config) {
    this.mysql = mysql;
    this.config = config;
  }

  async connect(environmentName) {
    if (!environmentName) {
      throw 'Please supply an environment name to connect'
    }
    if (!this.config[environmentName]) {
      throw 'Cannot find db environment data'
    }

    const config = this.config[environmentName];
    this.connection = await this.mysql.createConnection({
      host: config.host, user: config.username,
      password: config.password,
      database: 'contributor'
    });
  }

  getConnection() {
    if (!this.connection) {
      throw 'Database not connected';
    }

    return this.connection;
  }

  dropDatabase(database) {
    return this.getConnection().query(
      `DROP DATABASE IF EXISTS ${database}`
    );
  }

  createDatabase(database) {
    this.getConnection().query(
      `CREATE DATABASE IF NOT EXISTS ${database}`
    );
  }

  useDatabase(database) {
    return this.getConnection().query(
      `USE ${database}`
    );
  }

  getTables() {
    return ['contribution', 'donation', 'expenditure',
      'tag', 'expenditure_tag'];
  }

  /**
   * This will be replaced with the migration system
   */
  createTables() {
    return Promise.all(
      this.getTables().map(table => this.createTable(table))
    );
  }

  /**
   * This will be replaced with the migration system
   */
  createTable(table) {
    return this.getConnection().query(
      `CREATE TABLE IF NOT EXISTS ${table} (id INTEGER)`
    );
  }

  truncateTables() {
    return Promise.all(
      this.getTables().map(table => this.truncateTable(table))
    );
  }

  truncateTable(table) {
    return this.getConnection().query(
      `TRUNCATE TABLE ${table}`
    );
  }

  close() {
    this.getConnection().close();
  }

}

Наконец, вот фактический тест:

const mysql = require('mysql2/promise');
import TestDatabase from '../TestDatabase';
var config = require('../../config/config.json');

let database = new TestDatabase(mysql, config);

console.log('Here is the test class');


describe('Database tests', () => {

  beforeEach(() => {
    database.connect('test').then(() => {
      return database.useDatabase('contributor_test');
    }).then (() => {
      return database.truncateTables();
    }).catch(() => {
      console.log('Failed to clear down database');
    });
  });

  afterAll(async () => {
    await database.getConnection().close();
  });

  test('Describe this demo test', () => {
    expect(true).toEqual(true);
  });

});

Вывод

Как вы можете см. У меня есть журналы консоли, и вот их неожиданный порядок:

  1. «Вот bootstrap»
  2. «Вот тестовый класс»
  3. Завершение тестов sh здесь
  4. «Не удалось очистить базу данных»
  5. «Завершена однократная настройка базы данных»
  6. Jest reports «Jest не завершился через одну секунду после тестовый запуск завершен. Обычно это означает, что есть асинхронные операции, которые не были остановлены в ваших тестах. "
  7. Jest зависает, требуется ^ C для выхода

Я хочу:

  1. «Вот bootstrap»
  2. «Завершена однократная настройка БД»
  3. «Вот тестовый класс»
  4. Нет ошибки при вызове truncateTables

Я подозреваю, что ошибка базы данных заключается в том, что операции TRUNCATE не работают, потому что таблицы еще не существуют. Конечно, если бы команды выполнялись в правильном порядке, они бы были!

Примечания

Первоначально я импортировал mysql вместо mysql/promise и обнаружил в другом месте в Stack Overflow, что без обещаний нужно добавлять обратные вызовы к каждой команде. Это сделало бы беспорядочный файл установки - каждая из операций connect, drop db, create db, use db, create tables, close должна появиться в глубоко вложенной структуре обратного вызова. Я, вероятно, мог бы это сделать, но это немного неприятно.

Я также пробовал писать установочный файл, используя * 1 082 * против всех операций с БД, возвращающих обещание. Однако это означало, что мне пришлось объявить initDatabase как async, что, как мне кажется, означает, что я больше не могу гарантировать, что весь установочный файл будет запущен первым, что по сути является той же проблемой, что и у меня сейчас.

Я заметил, что большинство служебных методов в TestDatabase возвращают обещание, и мне это очень нравится. Однако connect - это странность - я хочу, чтобы это сохраняло соединение, поэтому меня смущало, могу ли я вернуть Promise, учитывая, что Promise не является соединением. Я только что попытался использовать .then() для сохранения соединения, например:

    return this.mysql.createConnection({
      host: config.host, user: config.username,
      password: config.password
    }).then((connection) => {
      this.connection = connection;
    });

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

Я вкратце подумал, что использование двух соединений может быть проблемой, если таблицы, созданные в одном соединении, нельзя увидеть, пока это соединение не будет закрыто. Основываясь на этой идее, возможно, мне стоит попробовать подключиться в установочном файле и каким-то образом повторно использовать это соединение (например, используя пул соединений mysql2). Однако мои чувства подсказывают мне, что это действительно проблема с Promise, и мне нужно решить, как завершить sh мой db init в установочном файле, прежде чем Jest попытается перейти к выполнению теста.

Что может Я попробую дальше? Я готов отбросить mysql2/promise и вернуться к mysql, если это лучший подход, но я бы предпочел упорствовать (и полностью проверять) обещаниями, если это вообще возможно.

Ответы [ 2 ]

1 голос
/ 06 мая 2020

Вам необходимо await ваш database.connect() в beforeEach().

0 голосов
/ 06 мая 2020

У меня есть решение. Я еще не au fait с тонкостями Jest, и мне интересно, нашел ли я его только что.

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

Другими словами, сценарий bootstrap может использоваться только для синхронных вызовов.

Решение 1

Одно из решений - переместить доступную цепочку из файла bootstrap в новый beforeAll() хук. Я преобразовал метод connect так, чтобы он возвращал Promise, поэтому он ведет себя как другие методы, и, в частности, я return изменил значение цепочки Promise как в новом, так и в существующем хуке. Я считаю, что это уведомляет Jest о том, что обещание должно быть разрешено до того, как могут произойти другие вещи. соединение не нужно закрывать и открывать заново.

Вот неасинхронная c версия connect в классе TestDatabase на go с вышеуказанными изменениями:

  connect(environmentName) {
    if (!environmentName) {
      throw 'Please supply an environment name to connect'
    }
    if (!this.config[environmentName]) {
      throw 'Cannot find db environment data'
    }

    const config = this.config[environmentName];

    return this.mysql.createConnection({
      host: config.host, user: config.username,
      password: config.password
    }).then(connection => {
      this.connection = connection;
    });
  }

Недостаток этого решения:

  • Мне нужно вызывать этот код инициализации в каждом тестовом файле (дублирует работу, которую я хочу выполнить только один раз), или
  • У меня есть для вызова этого кода инициализации только в первом тесте (который немного хрупок, я предполагаю, что тесты выполняются в алфавитном порядке?)

Решение 2

Более очевидное решение - что я могу поместить код инициализации базы данных в совершенно отдельный процесс, а затем изменить настройки package.json:

"test": "node ./bin/initdb.js && jest tests"

Я не пробовал этого, но я почти уверен, что это сработает - даже если код инициализации JavaScript, это будет Мне нужно завершить sh всю свою асинхронную c работу перед выходом.

...