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_examplesdatatypes中的数据格式检查器:check_is_mtype、check_is_scitype、check_raiseutils中的各种实用工具,尤其是在_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为模型。以及,一组用于测试接口是否符合估计器类型基类的测试。测试应涵盖正面案例,以及在负面案例中测试是否引发信息性错误消息。