Javascript 如何确保首先运行Jest引导文件? 介绍

Javascript 如何确保首先运行Jest引导文件? 介绍,javascript,async-await,jestjs,es6-promise,Javascript,Async Await,Jestjs,Es6 Promise,我被Jest测试框架中使用的数据库承诺所困扰。事情以错误的顺序运行,在我最近的一些更改之后,Jest没有正确完成,因为没有处理未知的异步操作。我对Node/Jest相当陌生 这就是我要做的。我正在一个多Docker容器环境中设置Jest,以调用内部API来测试它们的JSON输出,并运行服务函数来查看它们对测试环境MySQL数据库所做的更改。为此,我: 使用setupfileafterenvJest配置选项指向一个安装文件,我认为应该首先运行该文件 使用安装文件销毁测试数据库(如果存在)、重新创

我被Jest测试框架中使用的数据库承诺所困扰。事情以错误的顺序运行,在我最近的一些更改之后,Jest没有正确完成,因为没有处理未知的异步操作。我对Node/Jest相当陌生

这就是我要做的。我正在一个多Docker容器环境中设置Jest,以调用内部API来测试它们的JSON输出,并运行服务函数来查看它们对测试环境MySQL数据库所做的更改。为此,我:

  • 使用
    setupfileafterenv
    Jest配置选项指向一个安装文件,我认为应该首先运行该文件
  • 使用安装文件销毁测试数据库(如果存在)、重新创建它,然后创建一些表
  • 使用
    mysql2/promise
    对数据库执行操作
  • 在测试中使用
    beforeach(()=>{})
    截断所有表,以便插入每个测试数据,这样测试就不会相互依赖
我可以确认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
只是用户名/密码,不值得在这里显示

如您所见,此代码使用实用程序数据库类,如下所示:

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);
  });

});
输出 如您所见,我有一些控制台日志,这是它们意外的顺序:

  • “这是引导程序”
  • “这是测试类”
  • 测试到此结束
  • “无法清除数据库”
  • “一次性完成数据库设置”
  • Jest报告“Jest在测试运行完成后一秒钟内未退出。这通常意味着在测试中存在未停止的异步操作。”
  • Jest挂起,需要^C退出
  • 我想:

  • “这是引导程序”
  • “一次性完成数据库设置”
  • “这是测试类”
  • 调用
    truncateTables
  • 我怀疑数据库错误在于
    TRUNCATE
    操作失败,因为表还不存在。当然,如果命令以正确的顺序运行,它们会

    笔记 我最初导入的是
    mysql
    ,而不是
    mysql/promise
    ,从堆栈溢出的其他地方发现,如果没有promises,就需要向每个命令添加回调。这将使安装文件变得凌乱-每个操作connect、drop db、create db、use db、create tables、close都需要出现在一个深度嵌套的回调结构中。我可能会做,但有点恶心

    我还尝试使用
    await
    编写安装文件,以对抗所有的数据库操作。然而,这意味着我必须将
    initDatabase
    声明为
    async
    ,这意味着我不能再保证首先运行整个安装文件,这本质上与我现在遇到的问题相同

    我注意到,
    TestDatabase
    中的大多数实用方法都返回了一个承诺,我对此非常满意。然而,
    connect
    是一个奇怪的东西——我想用它来存储连接,所以我对是否可以返回承诺感到困惑,因为承诺不是连接。我刚刚尝试使用
    .then()
    存储连接,如下所示:

        return this.mysql.createConnection({
          host: config.host, user: config.username,
          password: config.password
        }).then((connection) => {
          this.connection = connection;
        });
    
    我想知道这是否可行,因为thenable链应该等待连接承诺解决,然后再转到列表中的下一项。但是,也会产生同样的错误

    我简单地认为使用两个连接可能会有问题,因为在一个连接中创建的表在该连接关闭之前无法看到。基于这个想法,也许我应该尝试在安装文件中连接,并以某种方式重新使用该连接(例如,通过使用mysql2连接池)。然而,我的感觉告诉我,这确实是一个承诺问题,我需要在Jest尝试继续测试执行之前,解决如何在安装文件中完成db init


    下一步我能试试什么?如果这是一个更好的方法,我愿意放弃
    mysql2/promise
    并回到
    mysql
    ,但如果可能的话,我宁愿坚持(并完全放弃)承诺。

    你需要
    等待
    你的
    数据库。在
    之前的
    中连接()

    我有一个解决方案。我还不太了解笑话的微妙之处,我想知道我是否刚刚找到了一个

    我的感觉是,因为从引导到Jest没有返回值,所以没有办法通知它需要等待承诺解决后再进行测试。这样做的结果是,在等待测试期间,这些承诺正在得到解决,这造成了一片混乱

    换句话说,引导脚本只能用于同步调用

    解决方案1 一种解决方案是将表格链从引导文件移动到新的
    beforeAll()
    hook。我将
    connect
    方法转换为返回承诺,因此它的行为与其他方法类似,值得注意的是,我在新钩子和现有钩子中都对承诺链的值进行了
    return
    ed。我相信这告诉Jest,在其他事情发生之前,承诺需要解决

    以下是新的测试文件:

    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');
    
    beforeAll(() => {
      return 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();
      }).catch((error) => {
        console.log('Init database failed: ' +  error);
      });
    });
    
    describe('Database tests', () => {
    
      beforeEach(() => {
        return database.connect('test').then(() => {
          return database.useDatabase('contributor_test');
        }).then (() => {
          return database.truncateTables();
        }).catch((error) => {
          console.log('Failed to clear down database: ' + error);
        });
      });
    
      /**
       * I wanted to make this non-async, but Jest doesn't seem to
       * like receiving a promise here, and it finishes with an
       * unhandled async complaint.
       */
      afterAll(() => {
        database.getConnection().close();
      });
    
      test('Describe this demo test', () => {
        expect(true).toEqual(true);
      });
    
    });
    
    事实上,这可能可以进一步简化,因为不需要关闭和重新打开连接

    这是非异步版本
      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;
        });
      }
    
    "test": "node ./bin/initdb.js && jest tests"