sktime
测试框架概述#
sktime
使用 pytest
来测试估计器的接口合规性,以及代码的正确性。本页概述了测试内容,并介绍了如何添加测试,或如何扩展测试框架。
测试模块架构#
sktime
测试分为三个层次,大致对应于估计器的继承层次。
“包级别”: 在
tests/test_all_estimators.py
中测试接口是否符合BaseObject
和BaseEstimator
规范“模块级别”:测试具体估计器与其科学类型基类的接口合规性,例如
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_instance
和 scenario
固定装置的循环,其中循环由 pytest
在 pytest_generate_tests
中的参数化来协调,这会自动用合适的循环装饰测试。值得注意的是,如果开发人员使用已经定义了循环的固定装置名称(例如 estimator_instance
),则测试中的循环不需要由开发人员编写。有关更多详细信息,请参见下文,或参阅 pytest 关于该主题的文档。
sktime
插件为 pytest
生成了这个的固定值元组。在上面的例子中,我们循环遍历了以下固定值列表:
estimator_instance
覆盖了从所有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参数化装饰,而是通过 pytest_generate_tests
生成fixture。
这是因为适用的场景(scenario
的固定值)依赖于 estimator_instance
固定装置,因为分类器的 fit
输入与预测器的 fit
输入不同。
参数化夹具#
sktime
使用 pytest
的夹具参数化来在夹具上循环执行测试,例如为所有估计器运行所有接口兼容性测试。有关夹具参数化的解释,请参阅 pytest 文档中的夹具参数化。
在实现方面,循环遍历fixture的操作由 pytest
在 pytest_generate_tests
中的参数化来协调,它会根据测试参数(如上述示例中的 estimator_instance
和 scenario
)自动为每个测试添加 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
: 测试场景,适用于estimator
或estimator_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)
将:
首先,调用
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
的文档字符串。
远程 CI 设置#
远程CI运行所有包级测试、模块级测试和低级测试,针对所有支持的操作系统(OS)和Python版本的组合。
estimators 包和模块级别分布在操作系统和Python版本组合中,以便:
每种组合只运行大约三分之一的估计器
对于给定的操作系统,给定的估计器至少运行一次。
给定的估计器至少运行一次用于某个Python版本
这是为了减少每个CI元素的运行时间和内存需求。
精确的逻辑将估计器、操作系统和Python版本映射为整数,并将估计器与操作系统和Python版本之和的模3匹配。
这个逻辑位于 tests.test_all_estimators
中的 subsample_by_version_os
,它在 BaseFixtureGenerator
的 pytest_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_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
来定义固定装置。
可用的测试夹具因模块而异,并在 pytest_generate_tests
的文档字符串中列出。
新的测试应尽可能使用这些夹具,但也可以通过 pytest
的基本夹具功能添加新的夹具。
如果要在整个模块中使用新的固定装置变量,或者依赖于现有的固定装置,请遵循下一节中的说明。
在可能的情况下,应使用场景来模拟通用方法调用(见上文),而不是直接创建和传递参数。场景将确保输入参数案例的一致覆盖。
添加夹具变量#
一次性固定装置变量(限定于一个或几个测试)应使用 pytest
的基本功能添加,例如不可变常量、pytest.fixture
或 pytest.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_sequence
和arg_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_estimators
或test_all_forecasters
为模型。以及,一组用于测试接口是否符合估计器类型基类的测试。测试应涵盖正面案例,以及在负面案例中测试是否引发信息性错误消息。