在持久模式的 schema 和事务容器中运行数据库测试

https://blueprints.launchpad.net/oslo/+spec/long-lived-transactionalized-db-fixtures

问题描述

OpenStack 应用目前没有一种标准的测试数据库支持代码的方法,该代码可以针对任何数量的潜在数据库后端进行测试。可用的技术本身也存在一些问题,使其在通用意义上不切实际。

  • 通常,特定 OpenStack 组件的测试套件最终被硬编码为在 SQLite 后端上运行。SQLite 的选择既是因为它的简单性,又是因为 schema 设置速度非常快,但它并不是 OpenStack 环境中实际使用的数据库。建立这种连接的方式通常被埋藏在多个文件中,并且没有选项可以更改使用的数据库。

  • 对于希望针对通用 schema 运行数百个测试的组件,没有解决方案,而无需为每个测试构建和拆解该 schema。这使得测试套件无法定期针对非 SQLite 后端运行,因为这种设置和拆解的成本过于耗时。一种典型的解决方法是在事务中运行测试,并在测试结束时回滚事务,从而允许 schema 保持存在。但是,对这种方法的简化、单数据库版本进行的实验失败了,因为使用了并行测试,并且能够允许测试本身保留基本的“提交/回滚”功能,而不会影响 fixture 的隔离性。

  • 虽然有一个系统允许应用程序针对其他后端运行一系列测试,称为“Opportunistic”(机会主义)测试套件,但目前“机会主义”套件会将这些测试固定到单个替代后端,例如 MySQL 或 Postgresql。虽然可以为每个所需的后端添加一个“stub”(存根)套件,以便在每个后端上运行相同的测试,但这种方法无法适应尝试针对应用程序未预料到的新后端运行套件,并且将责任放在开发人员身上,以确保适用于可变后端的测试都包含这些“stub”。

  • “机会主义”测试的连接 URL 也是固定的 URL,具有硬编码的用户名/密码,并且要求数据库位于 localhost 上。如果可以连接到这样的 URL,则使用它。这种硬编码和“机会主义”风格使得控制测试运行变得不方便;例如,一台同时运行 MySQL 和 Postgresql 服务器且具有硬编码用户名/密码的工作站无法控制测试运行以仅针对其中一个或两个数据库进行测试,或者根本不针对这些数据库进行测试;该套件将始终根据它们的存在,对两个数据库运行适用的测试。开发人员也无法将测试套件指向在其他主机上运行的服务器。

  • “机会主义”测试创建和删除数据库的系统,无法为新的后端提供足够的扩展性。

提议的变更

拟议的更改将确保在所有情况下都保留与当前测试设置技术的完全兼容性。“机会主义”系统将继续像现在一样工作,但提供环境选项来更改其行为。

关键组件将是 oslo/db/sqlchemy/provision.py 模块以及 oslo/db/sqlalchemy/test_base.py 模块。该 provision 模块当前包含处理在 Postgresql 和 MySQL 后端中创建和删除匿名数据库的逻辑。与 OpportunisticFixture 结合使用时,它会在每个测试设置/拆解中创建/删除一个匿名数据库。test_base 包含基本测试类和 fixture,这些类和 fixture 使用 provision.py 进行连接。

这些模块将得到增强,以包含以下部分中描述的几个新结构。

后端

这是一个可扩展的“后端”系统,基于一个名为 Backend 的基类,并由 provision 模块使用。Backend 将封装运行针对特定后端测试所需的任务,包括

  • 检测给定数据库是否可用的能力,无论是“机会主义”地还是通过环境变量

  • 创建、删除和检测“匿名”数据库是否存在的能力

  • 生成一个连接 URL 的能力,该 URL 将在连接时直接访问此数据库

  • 删除此类型数据库中的所有 schema 对象的能力。

这些功能的逻辑因后端而异。对于 SQLite,它可能使用内存数据库或基于文件的数据库,URL 方案是 SQLite 特定的,需要以特殊方式从匿名 schema 名称生成。对于 Postgresql 数据库,后端可以通过在删除数据库之前断开所有用户连接,或者能够处理 Postgresql 的 ENUM 类型(在擦除数据库中的对象时必须显式删除)来受益。

机会主义 URL

Backend 系统可以报告特定后端类型(例如 MySQL、SQlite、Postgresql 等)的数据库是否存在,这基于“机会主义”URL 系统。该系统默认情况下搜索具有固定连接配置的数据库。假设该系统包含一个 SQlite、Postgresql 和 MySQL 的 Backend 实现。这些后端实现中的每一个都报告一个候选“机会主义”URL;例如,一个 URL,如“postgresql://openstack_citest:openstack_citest@localhost”,可以测试其连接性。在没有任何配置的情况下,该系统将尝试使每个 BackendImpl 实现的“opportunstic”URL 可用。通过这种方式,该系统的工作方式与今天基本相同。

但是,为了使其在运行时可配置,我们将增强 OS_TEST_DBAPI_ADMIN_CONNECTION 环境变量的作用。当前系统允许此变量指定一个与 SQLite 测试链接的单个“override”(覆盖)URL,但不是“机会主义”URL。在新的系统中,它将允许一个以分号分隔的 URL 列表。例如,一个值允许测试同时针对特定的 SQLite 数据库和 Postgresql 数据库运行

export OS_TEST_DBAPI_ADMIN_CONNECTION=\
       /path/to/sqlite.db;\
       postgresql+psycopg2://scott:tiger@localhost/openstack

当存在显式的 OS_TEST_DBAPI_ADMIN_CONNECTION 时,这些 URL 将确定 BackendImpl 报告为可用的完整列表,并覆盖通常固定的“机会主义”URL。通过此功能,可以在运行时确定数据库后端的列表以及它们的完整连接信息。

配置

provision 模块将调用 Backend,以生成一个在数据库、schema 和事务三个层面上工作的“provisioning”(配置)系统。这些资源的管理将在任何数量的测试跨度内进行维护。

“数据库”通常会在单个 Python 进程中运行的所有测试跨度内,按后端维护。通过确保为给定的后端创建每个进程的匿名数据库,可以在并行运行测试而无需担心并发测试相互冲突的情况下安全地运行测试套件。当前的方法是每个测试创建和删除此数据库;允许相同的数据库在整个运行过程中持续存在将减少负载和复杂性。

“schema”由一组表和其他 schema 结构组成,这些结构是在数据库中创建的。大多数 OpenStack 应用程序在其模型中运行测试,其测试在一个 schema 中运行。其中大多数测试只需要在这些 schema 中执行数据操作;第二类测试,“migration”(迁移)测试不太常见,并且需要实际创建和删除这些 schema 的组件。

为了支持对固定 schema 练习数据操作的测试,provisioning 系统将在即将使用新创建的数据库时,调用一个特定于应用程序的“create schema”(创建 schema)hook,在所谓的“schema scope”(schema 范围)内。然后,只要继续调用也指定相同范围的其他测试,该 schema 将保持原样。 “schema scope”是一个字符串符号名称,任何数量的测试都可以引用它,以声明它们都在同一个 schema 中运行。例如,如果 Nova 中的四个不同的测试套件都声明它们的“SCHEMA_SCOPE”是“nova-cells”,并且这些套件都引用了一个生成 nova 模型“create schema”函数,则“create schema”函数将只被调用一次,然后所有四个测试套件将完全针对目标数据库运行。通过事务回滚来实现对这些测试所做的更改的数据清理,而不是通过删除整个数据库。

为了支持正在测试 schema 迁移并希望创建和删除自己的 schema 元素的测试,这些测试指定“SCHEMA_SCOPE”为 None;provisioning 系统将向这些测试提供一个空数据库,并且在释放配置后,将对仍然存在的任何 schema 对象执行 DROP 操作。

“transaction”(事务)是一个可选的单元,它在每个测试的基础上构建和拆解。当测试基础指定它希望具有“transactional”(事务性)支持时,会使用此功能,这表示当指定非 None“SCHEMA_SCOPE”时。此功能使用 SQLAlchemy 的 Engine 和 Connection 系统,以便透明地向测试提供模拟“transaction”环境。在此环境中,任何“commit”(提交)事务的调用都不会真正提交。测试能够发出通过在 SAVEPOINT 中包装环境来实现回滚的回滚。

SQLite 后端长期以来一直存在 SAVEPOINT 问题,但是为了支持此功能,后端在 oslo.db 中使用最新的 hook 进行修复;有关审查,请参见 https://review.openstack.org/#/c/113152/

Fixture 集成

provisioning 系统将通过利用 testresources 库集成到测试套件中,该库提供了一个在多个测试跨度内持续存在的资源分配系统。 testresources 的工作原理是维护依赖树中的各种资源的状态,该状态在许多测试进行时被跟踪。只有当给定资源报告自身为“dirty”(脏)时,它才会被拆解以进行下一个测试,并且最终拆解仅在不再需要该资源时才会发生。

默认情况下,使用 testresources 的测试将正常运行,但是它们需要的资源将在每个测试的基础上完全创建和删除,除非采取特定于 testtools 包的额外步骤。因此,测试将与任何样式的测试运行程序兼容,但是优化资源需要使用 testr 或 testtools 运行程序,或者使用一些额外的操作,标准的 Python unittest 运行程序。

为了优化多个测试之间的资源,测试必须组装到 testresources 提供的 OptimisingTestSuite 对象中。集成 OptimisingTestSuite 通常需要 unittest 支持的 load_tests() 指令在单个测试模块或包级别(例如 __init__.py)中声明,这将用一个将测试组装到主 OptimisingTestSuite 中的系统替换通常的测试发现系统。我们假设我们可以提供一个 oslo.db 指令,可以放入测试套件的顶级 __init__.py 文件中,以提供此效果。

为了与 testresources 集成,“database”(数据库)、“schema”(模式)和“transaction”(事务)的概念将实现为单独的测试资源对象类型。

场景

场景指的是使用像 testscenarios 这样的工具,以便可以针对不同的后端多次运行单个测试。现有的 Opportunistic fixture 系统将得到增强,以便“DRIVER”属性,现在指的是一种类型的数据库后端,可以引用一组类型。然后,每个测试将针对 Backend 系统认为可用的那些驱动程序运行。

在实际测试中的用法

实际测试通过使用 oslo.db.sqlalchemy.DbTestCase 来利用该系统。此测试用例超类的工作方式与以前一样,提供 self.sessionself.engine 成员用于数据库连接。但是,该类现在可以通过类级别注释来标记它适合哪些数据库以及哪个 schema。例如,Nova 可以建议一个针对 Nova schema 运行的测试套件,以及针对 SQLite、Postgresql 和 MySQL 运行,如下所示

class SomeNovatest(DbTestCase):

    SCHEMA_SCOPE = "nova-cells"
    DRIVER = ('sqlite', 'postgresql', 'mysql')

    def generate_schema(self, engine):
        """Generate schema objects to be used within a test."""

        nova.create_all_tables(engine)

    def test_something(self):
        # do an actual test

上面的类指定如何在 generate_schema() 方法中生成 schema,该方法由 provisioning 系统调用,以生成与“nova-cells”schema 范围对应的 schema。由于许多测试套件可以使用相同的 generate_schema() 方法,因此最好将 generate_schema()SCHEMA_SCOPE="nova-cells" 在一个通用的 mixin 上链接起来。

为了与测试资源集成,上述指令集将被用于计算通过 `.resources` hook 交付的完整测试资源管理器对象集;这是一个绑定到 `DbTestCase` 类本身的属性,测试资源会查找该属性以确定特定测试需要哪些类型的资源对象。该实现对 `.resources` 使用 Python 描述符,以便其值是基于每个测试动态确定的。

备选方案

选择使用测试资源是基于对其他两个不使用它的变体的决策。这里讨论了所有三个变体。

  • testresources 库提供了一种跨测试扩展资源的方式,它与标准的 Python unittest.TestSuite 对象的机制集成,以及 load_tests() hook,该 hook 用于将 TestSuite 对象建立到单个 OptimisingTestSuite 中。这些机制在其他常用的测试运行器中(包括 nose 和 py.test)不可用或并非完全可用。

    使用 testresources 的优点包括它是与 testtools 的其他用法一起的标准系统,并提供了一个复杂的系统来组织测试,以充分利用每个测试声明的资源。它的测试管理器 API 建立了一个清晰的声明和依赖系统,用于配置系统中提出的各种类型的资源。

    缺点是优化行为仅在 testtools 样式的运行中可用,或者在 unittest 样式的运行中可用,如果采取额外的步骤来集成 OptimisingTestSuite,因为 unittest 本身似乎不遵守包级别的 load_tests() hook。

    仍然需要解决的是顶层 `__init__.py` 文件中实现的 load_tests() hook 的一些剩余问题,当“start”目录是该目录本身时;似乎在这种情况下会跳过 `load_tests()` hook,并且可能需要重新组织 oslo.db 的测试,以便所有测试都可以从命名的包中加载。但是请注意,这个问题不是一个阻碍;`load_tests()` hook 在放置在特定的测试模块或加载为包的 `__init__.py` 文件中时,工作正常,而这正是绝大多数 openstack 测试套件的情况。

  • 使用 Testr 的“实例配置”hook 维护对每个进程的测试套件开始/结束的感知。这些 hook 允许在测试运行之前生成一组固定的数据库名称,将此名称提供给每个子进程中的配置系统,最后在所有测试套件完成后,为所有可用后端发出每个数据库名称的 DROP 命令。该系统可以延迟创建数据库,并且仅删除实际创建的数据库。

    配置如下所示

    instance_provision=${PYTHON:-python} -m oslo.db.sqlalchemy.provision echo $INSTANCE_COUNT
    instance_execute=OSLO_SCHEMA_TOKEN=$INSTANCE_ID $COMMAND
    instance_dispose=${PYTHON:-python} -m oslo.db.sqlalchemy.provision drop --conditional $INSTANCE_IDS
    

    “实例配置”hook 实际上不会创建任何数据库;只有将在测试运行期间请求特定后端数据库时使用的数据库的字符串名称。“实例释放”hook 然后将这些名称传递给“drop”命令,如果显示数据库存在,则会在所有可能的后端上删除命名的数据库;否则,跳过该名称。

    该系统运行效率几乎与 testresources 系统一样高,并且在使用其他测试运行器时仍然可以优雅地降级。

    该系统的优点是它独立于 unittest 的机制,并且在 testr 中只有非常简单的 hook,可以轻松地使其与其他测试运行器一起工作。它也不需要任何包或模块级别的 load_tests() hook,也不涉及对测试顺序的任何更改。

    缺点包括它更像是一种“自制”方法,重新发明了 testresources 已经做很多事情。探索增强 testresources 本身,使其更易于与其他类型的测试运行器集成,可能更有优势。

  • 通过确保套件始终在与 testr hook 相同的命令和环境变量设置中运行的特殊 shell 脚本中运行,来维护对测试套件开始/结束过程的感知。

    该系统与使用 testr hook 的系统类似,并且这两个系统可以共存。

    缺点不仅包括 testr 方法的缺点,还包括 shell 脚本很复杂且临时,因此从某种意义上说,这里重新发明的代码更多。

Impact on Existing APIs

希望利用该系统的测试套件需要基于 DbTestCase 的新机制,并重新设计任何现有系统,以设置连接或模式,使其在新的系统中工作。他们还需要某种模块或包级别的 load_tests() 指令,以便加载 OptimisingTestSuite 系统。

安全影响

性能影响

此蓝图的关键交付成果是显著提高希望在异构数据库后端上针对通用模式运行许多测试的测试套件的性能。

Configuration Impact

基于集成方法,测试运行器的配置可能会受到影响。这些更改应该可以交付到 gate 运行,而无需对 gate 进行任何直接更改。

开发人员影响

开发人员应该了解 DbTestCase 基本 fixture,其含义,并希望将其用于以严肃的方式针对数据库工作的测试。

Testing Impact

该系统的各个组件将在 oslo.db 中拥有自己的测试,以确保数据库设置/拆卸以及确保事务容器按预期工作。

实现

负责人

Mike Bayer 已经基于使用 testresources 的原型化了所有内容,除了场景支持。

Robert Collins 也在为确保 testtools 加载所有 Python 包作为包的问题做出贡献,以便在所有情况下都运行 load_tests() hook。

里程碑

N/A

工作项

  • 构建配置系统和后端系统。这已经完成,包括与 testresources 的集成。

  • 构建测试场景集成 - 仍然是一个 TODO

  • 实现 load_tests() 将被集成的手段,这已经完成。

  • 文档

孵化

N/A

采用

Nova、Neutron 和 Keystone 可能是好的起点。

oslo.db

预计 API 稳定

未知

文档影响

关于 DbTestCase 的文档字符串。

依赖项

Testresources 和 testscenarios。

参考资料

原始 bug:https://bugs.launchpad.net/oslo/+bug/1339206

当前原型:https://review.openstack.org/#/q/status:open+project:openstack/oslo.db+branch:master+topic:bug/1339206,n,z

注意

本作品采用知识共享署名 3.0 非移植许可协议授权。 http://creativecommons.org/licenses/by/3.0/legalcode