Введение
Я застрял с обещаниями базы данных, которые я использую внутри среды тестирования 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);
});
});
Вывод
Как вы можете см. У меня есть журналы консоли, и вот их неожиданный порядок:
- «Вот bootstrap»
- «Вот тестовый класс»
- Завершение тестов sh здесь
- «Не удалось очистить базу данных»
- «Завершена однократная настройка базы данных»
- Jest reports «Jest не завершился через одну секунду после тестовый запуск завершен. Обычно это означает, что есть асинхронные операции, которые не были остановлены в ваших тестах. "
- Jest зависает, требуется ^ C для выхода
Я хочу:
- «Вот bootstrap»
- «Завершена однократная настройка БД»
- «Вот тестовый класс»
- Нет ошибки при вызове
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
, если это лучший подход, но я бы предпочел упорствовать (и полностью проверять) обещаниями, если это вообще возможно.