sktime
测试框架概述#
sktime
使用 pytest
测试估计器的接口合规性以及代码的正确性。本页概述了这些测试,并介绍了如何添加测试或扩展测试框架。
测试模块架构#
sktime
测试分三个层面进行,大致对应于估计器的继承层面。
“包层面”:测试与
BaseObject
和BaseEstimator
规范的接口合规性,位于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.py
或distances/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_instance
和 scenario
fixture 的循环,其中循环由 pytest_generate_tests
中的 pytest
参数化协调,它会自动为测试添加一个合适的循环。值得注意的是,如果测试使用了已经定义了循环的 fixture 名称(例如 estimator_instance
),则开发者无需编写测试中的循环。详见下方内容,或查阅 pytest documentation on the topic。
sktime
的 pytest
插件为此生成 fixture 值的元组。在上面的示例中,我们循环遍历以下 fixture 列表
estimator_instance
循环遍历估计器实例,这些实例通过create_test_instances_and_names
从所有sktime
估计器中获取,create_test_instances_and_names
根据估计器类的get_test_params
中的参数设置构建实例。scenario
对象,它编码数据输入和对estimator_instance
的方法调用序列(将在下方详细解释)。
sktime
插件确保只检索适用于 estimator_instance
的 scenarios
。
在该示例中,scenario.run
命令等同于调用 estimator_instance.fit(**scenario_kwargs)
,其中 scenario_kwargs
由 scenario
生成。
需要注意的是,测试没有使用 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_instance
和 scenario
)自动为每个测试添加 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
:测试场景,适用于estimator
或estimator_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)
将会
首先,调用
estimator_instance.fit(y=_make_series(n_timepoints=20, random_state=RAND_SEED))
然后,调用
estimator_instance.predict(fh=1)
并将输出返回给result
。
“场景”的抽象允许跨多个方法指定多种参数组合。
方法 run
也有参数(method_sequence
和 arg_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_estimators
的 subsample_by_version_os
中,并在 BaseFixtureGenerator
的 pytest_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_mtype
、check_is_scitype
、check_raise
utils
中的杂项工具,特别是在_testing
中
跳过测试#
有时,跳过个别测试中的个别估计器可能是合理的。
这可以通过两种方式完成(目前,截至 0.9.0 版本)
将估计器或测试/估计器组合添加到相应的
_config
文件中的EXCLUDED_TESTS
或EXCLUDE_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.fixture
或 pytest.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_sequence
和default_arg_sequence
,字符串列表。如果调用run
,这些定义了方法调用的序列以及使用的参数集。两者都可以是类变量,或在构造函数中设置的对象变量。旁注:
method_sequence
和arg_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_estimators
或test_all_forecasters
进行建模。以及,一组用于测试与估计器类型基类接口合规性的测试。测试应涵盖正向情况,并测试在负向情况中是否抛出有用的错误信息。