sktime 测试框架概述#

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

测试模块架构#

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

  • “包级别”: 在 tests/test_all_estimators.py 中测试接口是否符合 BaseObjectBaseEstimator 规范

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

  • “低级”:在 tests 文件夹中的单独文件中测试估计器或其他代码的单个功能。

模块约定如下:

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

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

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

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

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

  • 每个对应于学习任务和估计器类型的测试模块,应在测试文件 test_all_[name_of_scitype].py 中包含模块级别的测试,以测试所有遵循该类型的估计器的接口合规性。例如,forecasting/tests/test_all_forecasters.py,或 distances/tests/test_all_dist_kernels.py

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

测试代码架构#

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

估计器测试使用 sktime 的框架插件来扩展 pytest_generate_tests,该插件参数化了估计器固定装置和数据输入场景。

一个说明性的例子#

从一个例子开始:

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 固定装置的循环,其中循环由 pytestpytest_generate_tests 中的参数化来协调,这会自动用合适的循环装饰测试。值得注意的是,如果开发人员使用已经定义了循环的固定装置名称(例如 estimator_instance),则测试中的循环不需要由开发人员编写。有关更多详细信息,请参见下文,或参阅 pytest 关于该主题的文档

sktime 插件为 pytest 生成了这个的固定值元组。在上面的例子中,我们循环遍历了以下固定值列表:

  • estimator_instance 覆盖了从所有 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参数化装饰,而是通过 pytest_generate_tests 生成fixture。

这是因为适用的场景(scenario 的固定值)依赖于 estimator_instance 固定装置,因为分类器的 fit 输入与预测器的 fit 输入不同。

参数化夹具#

sktime 使用 pytest 的夹具参数化来在夹具上循环执行测试,例如为所有估计器运行所有接口兼容性测试。有关夹具参数化的解释,请参阅 pytest 文档中的夹具参数化

在实现方面,循环遍历fixture的操作由 pytestpytest_generate_tests 中的参数化来协调,它会根据测试参数(如上述示例中的 estimator_instancescenario)自动为每个测试添加 mark.parameterize 装饰器。这与 pytest_generate_tests 的标准用法一致,详见 pytest 文档中关于使用 pytest_generate_tests 进行 高级fixture参数化 的部分。

目前,sktime 测试框架通过 mark.parameterize 为以下固定装置提供了自动参数化,用于模块级别的测试:

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

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

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

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

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

进一步的参数化可能会针对个别测试进行,通常在测试的文档字符串中解释其范围。

场景#

scenario 夹具包含方法调用的参数,以及方法调用的序列。

一个示例场景规范,来自 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 的文档字符串。

远程 CI 设置#

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

estimators 包和模块级别分布在操作系统和Python版本组合中,以便:

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

  • 对于给定的操作系统,给定的估计器至少运行一次。

  • 给定的估计器至少运行一次用于某个Python版本

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

精确的逻辑将估计器、操作系统和Python版本映射为整数,并将估计器与操作系统和Python版本之和的模3匹配。

这个逻辑位于 tests.test_all_estimators 中的 subsample_by_version_os,它在 BaseFixtureGeneratorpytest_generate_tests 中被调用,而 BaseFixtureGenerator 被所有 TestAll[estimator_type] 类继承。

默认情况下,按操作系统和Python版本进行子集设置是关闭的,但可以通过将 pytest 标志 matrixdesign 设置为 True 来开启(参见 conftest.py

扩展测试模块#

本节解释如何扩展测试模块。根据测试的主要变化,测试模块的更改可能是浅层的或深层的。按常见程度递减顺序:

  • 在添加新的估计器或实用功能时,编写低级测试以检查估计器的正确性。

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

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

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

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

添加低级测试#

低级测试是“自由格式”的,应遵循最佳 pytest 实践。pytest 测试应位于进行更改的模块的适当 tests 文件夹中。示例应位于添加的类或函数的文档字符串中。

对于一个名为 estimator_name 的附加估计器,测试文件应命名为 test_estimator_name.py

编写测试的有用功能:

  • 示例装置生成,通过 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 来定义固定装置。

可用的测试夹具因模块而异,并在 pytest_generate_tests 的文档字符串中列出。

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

如果要在整个模块中使用新的固定装置变量,或者依赖于现有的固定装置,请遵循下一节中的说明。

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

添加夹具变量#

一次性固定装置变量(限定于一个或几个测试)应使用 pytest 的基本功能添加,例如不可变常量、pytest.fixturepytest.mark.parameterize。如果这能使测试更具(而非更不)可读性,也可以考虑扩展 pytest_generate_tests

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

这需要:

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

  • 将函数赋值给 generator_dict["variablename"]

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

函数 _generate_[variable_name](test_name, **kwargs) 应该返回两个对象:

  • 一个要循环的夹具列表,用于在测试签名中出现时替换 variable_name

  • 一个长度相等的名称列表,第 i 个元素用作测试日志中第 i 个夹具的名称

该函数可以访问:

  • test_name,变量在其中被调用的测试名称。

这可以用于为特定测试自定义夹具列表,尽管这主要是为了通用行为设计的。一次性转义和类似的操作应避免在此处使用,而应通过 xfail 和类似的方式处理。

  • kwargs 中,较早出现在 fixture_sequence 中的夹具变量的值。

例如,estimator_instance 的值,如果这是测试中使用的变量。这可以用来使 variable_name 的固定装置列表依赖于其他固定装置变量的值。

添加或扩展场景#

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

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

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

特定估计器类型的场景可以在 utils/_testing/scenarios_[estimator_scitype] 中找到。所有场景都继承自该类型的基类,例如 ForecasterTestScenario。该基类为所有相同类型的场景定义了通用方法,如 is_applicable 或标签处理。

场景通常应定义:

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

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

  • 可选地,可以有一个 default_method_sequence 和一个 default_arg_sequence,它们是字符串列表。这些定义了在调用 run 时方法被调用的顺序及其参数集。两者都可以是类变量,或者在构造函数中设置的对象变量。

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

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

  • 可选地,一个 get_args 方法,允许覆盖从 args 中获取键的操作。例如,指定规则如“如果键以 predict_ 开头,总是返回…”

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

有关更多详细信息和预期签名,请参阅 TestScenario 的文档字符串(链接),或者检查任何场景基类,例如 ForecasterTestScenario

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

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

  • 覆盖指定基类接口行为的场景,在 utils/_testing/scenarios_[estimator_scitype] 中。这可以以 utils/_testing/scenarios_forecasting 或其他场景文件为模型。

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

  • 一个 tests/test_all_[estimator_scitype].py,位于模块的根目录。

  • 在这个文件中,通过 pytest_generate_fixtures 生成适当的夹具。这可以以 test_all_estimatorstest_all_forecasters 为模型。

  • 以及,一组用于测试接口是否符合估计器类型基类的测试。测试应涵盖正面案例,以及在负面案例中测试是否引发信息性错误消息。