sktime 测试框架概述#

sktime 使用 pytest 测试估计器的接口合规性以及代码的正确性。本页概述了这些测试,并介绍了如何添加测试或扩展测试框架。

测试模块架构#

sktime 测试分三个层面进行,大致对应于估计器的继承层面。

  • “包层面”:测试与 BaseObjectBaseEstimator 规范的接口合规性,位于 tests/test_all_estimators.py 中。

  • “模块层面”:测试具体估计器与其 scitype 基类的接口合规性,例如 forecasting/tests/test_all_forecasters.py

  • “底层”:测试估计器或其他代码的个体功能,位于 tests 文件夹中的独立文件中。

模块约定如下

  • 每个模块包含一个 tests 文件夹,其中包含针对该模块的测试。

  • 子模块也可以包含 tests 文件夹。

  • tests 文件夹可以包含 _config.py 文件,用于收集该模块的测试配置设置。

  • 测试用的通用工具位于模块 utils._testing 中。

  • 这些工具的测试应包含在 utils._testing.tests 文件夹中。

  • 每个对应于学习任务和估计器 scitype 的测试模块应包含模块级别测试,位于 test_all_[scitype_名称].py 文件中,该文件测试所有遵循该 scitype 的估计器的接口合规性。例如,forecasting/tests/test_all_forecasters.pydistances/tests/test_all_dist_kernels.py

  • 学习任务特定的测试不应重复 test_all_estimators.py 中的包级别通用估计器测试。

测试代码架构#

sktime 测试文件应尽可能使用 pytest 的最佳实践,例如 fixture 或测试参数化,而不是自定义逻辑,详见 pytest documentation

估计器测试使用 sktime 的框架插件,用于 pytest_generate_tests,它参数化估计器 fixture 和数据输入场景。

一个说明性示例#

从一个示例开始

def test_fit_returns_self(estimator_instance, scenario):
   """Check that fit returns self."""
   fit_return = scenario.run(estimator_instance, method_sequence=["fit"])
   assert (
      fit_return is estimator_instance
   ), f"Estimator: {estimator_instance} does not return self when calling fit"

该测试构成了对 estimator_instancescenario fixture 的循环,其中循环由 pytest_generate_tests 中的 pytest 参数化协调,它会自动为测试添加一个合适的循环。值得注意的是,如果测试使用了已经定义了循环的 fixture 名称(例如 estimator_instance),则开发者无需编写测试中的循环。详见下方内容,或查阅 pytest documentation on the topic

sktimepytest 插件为此生成 fixture 值的元组。在上面的示例中,我们循环遍历以下 fixture 列表

  • estimator_instance 循环遍历估计器实例,这些实例通过 create_test_instances_and_names 从所有 sktime 估计器中获取,create_test_instances_and_names 根据估计器类的 get_test_params 中的参数设置构建实例。

  • scenario 对象,它编码数据输入和对 estimator_instance 的方法调用序列(将在下方详细解释)。

sktime 插件确保只检索适用于 estimator_instancescenarios

在该示例中,scenario.run 命令等同于调用 estimator_instance.fit(**scenario_kwargs),其中 scenario_kwargsscenario 生成。

需要注意的是,测试没有使用 fixture 参数化进行装饰,fixture 而是由 pytest_generate_tests 生成。

这样做的原因是适用的场景(scenario 的 fixture 值)取决于 estimator_instance fixture,因为分类器 fit 的输入与预测器 fit 的输入不同。

参数化 fixture#

sktime 使用 pytest fixture 参数化来循环执行测试,例如对所有估计器运行所有接口兼容性测试。有关 fixture 参数化的解释,请参阅 pytest documentation on fixture parameterization 中的一般说明。

在实现方面,fixture 的循环由 pytest_generate_tests 中的 pytest 参数化协调,它根据测试参数(上面示例中的 estimator_instancescenario)自动为每个测试添加 mark.parameterize 装饰器。这符合 pytest_generate_tests 的标准用法,请参阅 pytest 文档中关于使用 pytest_generate_tests 进行 advanced fixture parameterization 的章节。

目前,sktime 测试框架通过 mark.parameterize 为模块级别测试中的以下 fixture 提供自动参数化

  • estimator:所有估计器类,继承自给定模块的基类。

  • 在包级别测试 test_all_estimators 中,该基类是 BaseEstimator

  • estimator_instance:所有估计器测试实例,通过 create_test_instances_and_names 从所有 sktime 估计器中获取。

  • scenario:测试场景,适用于 estimatorestimator_instance

  • 场景在 utils/_testing/scenarios_[estimator_scitype] 中指定。

个别测试可能会进行进一步的参数化,范围通常在测试 docstrings 中解释。

场景#

scenario fixture 包含方法调用的参数以及方法调用的序列。

一个场景规范示例,来自 utils/_testing/scenarios_forecasting

class ForecasterFitPredictUnivariateNoXLateFh(ForecasterTestScenario):
   """Fit/predict only, univariate y, no X, no fh in predict."""

   _tags = {"univariate_y": True, "fh_passed_in_fit": False}

   args = {
      "fit": {"y": _make_series(n_timepoints=20, random_state=RAND_SEED)},
      "predict": {"fh": 1},
   }
   default_method_sequence = ["fit", "predict"]

场景 ForecasterFitPredictUnivariateNoXLateFh 编码了通过 scenario 实例应用于 estimator_instance 的指令。调用 result = scenario.run(estimator_instance) 将会

  1. 首先,调用 estimator_instance.fit(y=_make_series(n_timepoints=20, random_state=RAND_SEED))

  2. 然后,调用 estimator_instance.predict(fh=1) 并将输出返回给 result

“场景”的抽象允许跨多个方法指定多种参数组合。

方法 run 也有参数(method_sequencearg_sequence),允许覆盖方法序列,例如以不同顺序运行它们,或只运行其中的一个子集。

场景还提供了一个方法 scenario.is_applicable(estimator),它返回一个布尔值,表示 scenario 是否适用于 estimator。例如,包含单变量数据的场景不适用于多变量预测器,并在 fit 方法调用中引发异常。不适用的场景可以在正向测试中被过滤掉,在负向测试中被包含进来。默认情况下,sktime 实现的 pytest_generate_tests 只传递适用的场景。

此外,场景继承自 BaseObject,这允许将 sktime 标签系统与场景一起使用。

有关场景的更多详细信息,请查阅 BaseScenario 的 docstring。

远程 CI 设置#

远程 CI 运行所有包级别测试、模块级别测试和底层测试,覆盖所有支持的操作系统 (OS) 和 Python 版本组合。

估计器包和模块级别测试分布在不同的 OS 和 Python 版本组合中,以便

  • 每个组合只运行大约三分之一的估计器

  • 给定估计器在给定 OS 上至少运行一次

  • 给定估计器在给定 Python 版本上至少运行一次

这样做是为了减少每个 CI 元素的运行时和内存需求。

精确的逻辑将估计器、OS 和 Python 版本映射到整数,并将估计器与 OS 和 Python 版本之和模 3 的结果匹配。

该逻辑位于 tests.test_all_estimatorssubsample_by_version_os 中,并在 BaseFixtureGeneratorpytest_generate_tests 中被调用,所有 TestAll[estimator_type] 类都继承自 BaseFixtureGenerator

默认情况下,按 OS 和 Python 版本进行的子集划分是关闭的,但可以通过将 pytest 标志 matrixdesign 设置为 True 来开启(详见 conftest.py)。

扩展测试模块#

本节解释如何扩展测试模块。根据主要测试的变更,对测试模块的更改可能是浅层或深层的。按普遍性递减顺序排列

  • 添加新的估计器或工具功能时,编写检查估计器正确性的底层测试。

  • 这些通常只使用 pytest 中最简单的惯用法(例如,fixture 参数化)。

  • 新的估计器也会被现有的模块和包级别测试自动发现并循环遍历。

  • 引入或更改基类级别接口点通常需要添加模块级别测试,以及添加或修改具有这些接口点特定功能的场景。极少数情况下,这可能需要更改包级别测试。

  • 重大的接口变更或模块添加可能需要编写整个测试套件,以及更改或添加包级别测试。

添加底层测试#

底层测试是“自由形式”的,应遵循 pytest 的最佳实践。pytest 测试应位于进行更改的模块的相应 tests 文件夹中。示例应位于添加的类或函数的 docstring 中。

对于名称为 estimator_name 的已添加估计器,测试文件应命名为 test_estimator_name.py

编写测试的有用功能

  • 示例 fixture 生成,通过 datatypes.get_examples

  • datatypes 中的数据格式检查器:check_is_mtypecheck_is_scitypecheck_raise

  • utils 中的杂项工具,特别是在 _testing

跳过测试#

有时,跳过个别测试中的个别估计器可能是合理的。

这可以通过两种方式完成(目前,截至 0.9.0 版本)

  • 将估计器或测试/估计器组合添加到相应的 _config 文件中的 EXCLUDED_TESTSEXCLUDE_ESTIMATORS 中。

  • pytest_generate_fixtures 中使用的 is_excluded 方法中添加检查条件,可能仅在该测试模块支持此功能时进行。

应尽量避免直接在测试中跳过测试,例如通过 if isinstance(estimator_instance, MyClass) 进行。

添加包或模块级别测试#

模块级别测试使用 pytest_generate_tests 来定义 fixture。

可用的 fixture 因模块而异,并列在 pytest_generate_tests 的 docstring 中。

如果可能,新测试应使用这些 fixture,但也可以通过 pytest 的基本 fixture 功能添加新的 fixture。

如果新 fixture 变量将在整个模块中使用,或者依赖于现有 fixture,则应遵循下一节中的说明。

在可能的情况下,应使用场景来模拟通用方法调用(见上文),而不是直接创建和传递参数。场景将确保输入参数情况的一致覆盖。

添加 fixture 变量#

一次性 fixture 变量(局限于一个或几个测试)应使用 pytest 的基本功能添加,例如不可变常量、pytest.fixturepytest.mark.parameterize。在这种情况下,如果能使测试更易读(而不是更难读),也可以考虑扩展 pytest_generate_tests

相比之下,在整个模块或包级别测试中使用的 fixture 通常应添加到由 pytest_generate_tests 调用的 fixture 生成过程中。

这需要

  • 添加一个函数 _generate_[变量名](test_name, **kwargs),如下所述

  • 将该函数赋值给 generator_dict["变量名"]

  • pytest_generate_tests 中的 fixture_sequence 列表中添加新变量

函数 _generate_[变量名](test_name, **kwargs) 应返回两个对象

  • 一个要循环遍历的 fixture 列表,用于替换出现在测试签名中的 variable_name

  • 一个等长的名称列表,第 i 个元素在测试日志中用作第 i 个 fixture 的名称

该函数可以访问

  • test_name,调用该变量的测试的名称。

这可以用于为特定测试定制 fixture 列表,尽管这主要用于通用行为。应避免在这里进行一次性跳过或类似操作,而应使用 xfail 等处理。

  • fixture_sequence 中较早出现的 fixture 变量的值,位于 kwargs 中。

例如,如果 estimator_instance 是测试中使用的变量,则其值。这可以用于使 variable_name 的 fixture 列表依赖于其他 fixture 变量的值。

添加或扩展场景#

如果需要测试新的方法/输入值组合,可以添加或修改场景。两个主要选项是

  • 添加一个新的场景,类似于现有估计器 scitype 的场景。这是需要覆盖新的输入条件时的常见情况。

  • 向现有场景添加方法或参数键。这是需要覆盖新方法或方法序列时的常见情况。为此,参数应添加到现有场景的 args 键中。

特定估计器 scitype 的场景位于 utils/_testing/scenarios_[estimator_scitype] 中。所有场景都继承自该 scitype 的基类,例如 ForecasterTestScenario。该基类定义了所有同类型场景的通用内容,例如 is_applicable 或标签处理。

场景通常应定义

  • 一个 args 参数:一个字典,包含任意键(通常是方法名称)。

  • args 参数可以作为类变量设置,或由构造函数设置。

  • 可选地,default_method_sequencedefault_arg_sequence,字符串列表。如果调用 run,这些定义了方法调用的序列以及使用的参数集。两者都可以是类变量,或在构造函数中设置的对象变量。

  • 旁注:method_sequencearg_sequence 也可以在 run 中指定。如果没有传递,将进行默认处理(首先相互默认,然后默认到 default_etc 变量)。

  • 可选地,一个 _tags 字典,它是一个 BaseObject 标签字典,其行为与估计器的完全相同。

  • 可选地,一个 get_args 方法,允许覆盖从 args 中检索键的方式。例如,指定诸如“如果键以 predict_ 开头,则始终返回……”之类的规则。

  • 可选地,一个 is_applicable 方法,允许将场景与估计器进行比较。例如,比较场景和估计器是否都是多变量的。

有关更多详细信息和预期签名,请查阅 TestScenario 的 docstring(link),和/或检查任何场景基类,例如 ForecasterTestScenario

为新的估计器类型创建测试#

如果添加了新的估计器类型模块,需要为模块级别测试创建多项内容

  • 用于覆盖指定基类接口行为的场景,位于 utils/_testing/scenarios_[estimator_scitype] 中。这可以参考 utils/_testing/scenarios_forecasting 或其他场景文件来建模。

  • utils/_testing/scenarios_getter 的 dispatch 字典中的一行,它将场景链接到场景检索函数,例如 scenarios["forecaster"] = scenarios_forecasting

  • 模块根目录下的 tests/test_all_[estimator_scitype].py 文件。

  • 在该文件中,通过 pytest_generate_fixtures 进行适当的 fixture 生成。这可以参考 test_all_estimatorstest_all_forecasters 进行建模。

  • 以及,一组用于测试与估计器类型基类接口合规性的测试。测试应涵盖正向情况,并测试在负向情况中是否抛出有用的错误信息。