在 Bazel 中测试 Starlark 代码有多种不同的方法。此页面按使用情形汇总了当前的最佳实践和框架。
测试规则
Skylib 有一个名为 unittest.bzl
的测试框架,用于检查规则的分析时行为,例如其操作和提供程序。此类测试称为“分析测试”,目前是测试规则内部运作的最佳选择。
注意事项:
测试断言发生在 build 内部,而不是单独的测试运行程序进程中。 由测试创建的目标的名称不得与来自其他测试或 build 的目标发生冲突。测试期间发生的错误会被 Bazel 视为 build 损坏,而不是测试失败。
它需要相当多的样板代码来设置被测规则和包含测试断言的规则。此样板代码起初可能看起来令人望而生畏。请务必记住,宏在加载阶段进行评估并生成目标,而规则实现函数直到分析阶段才会运行。
分析测试旨在实现相对较小且轻量级的效果。分析测试框架的某些功能仅限于验证具有最大数量的传递依赖项(目前为 500)的目标。这是因为在较大规模的测试中使用这些功能会影响性能。
基本原则是定义一个依赖于被测规则的测试规则。这样,测试规则就可以访问被测规则的提供程序。
测试规则的实现函数会执行断言。如果存在任何失败,这些失败不会通过调用 fail()
立即引发(这会触发分析时构建错误),而是存储在生成的脚本中,该脚本会在测试执行时失败。
请参阅下文中的简短示例,以及检查操作的示例。
最小示例
//mypkg/myrules.bzl
:
MyInfo = provider(fields = {
"val": "string value",
"out": "output File",
})
def _myrule_impl(ctx):
"""Rule that just generates a file and returns a provider."""
out = ctx.actions.declare_file(ctx.label.name + ".out")
ctx.actions.write(out, "abc")
return
[MyInfo(val="some value", out=out)]
myrule = rule(
implementation = _myrule_impl,
)
//mypkg/myrules_test.bzl
:
load("@bazel_skylib//lib:unittest.bzl", "asserts", "analysistest")
load(":myrules.bzl", "myrule", "MyInfo")
# ==== Check the provider contents ====
def _provider_contents_test_impl(ctx):
env = analysistest.begin(ctx)
target_under_test = analysistest.target_under_test(env)
# If preferred, could pass these values as "expected" and "actual" keyword
# arguments.
asserts.equals(env, "some value", target_under_test[MyInfo].val)
# If you forget to return end(), you will get an error about an analysis
# test needing to return an instance of AnalysisTestResultInfo.
return analysistest.end(env)
# Create the testing rule to wrap the test logic. This must be bound to a global
# variable, not called in a macro's body, since macros get evaluated at loading
# time but the rule gets evaluated later, at analysis time. Since this is a test
# rule, its name must end with "_test".
provider_contents_test = analysistest.make(_provider_contents_test_impl)
# Macro to setup the test.
def _test_provider_contents():
# Rule under test. Be sure to tag 'manual', as this target should not be
# built using `:all` except as a dependency of the test.
myrule(name = "provider_contents_subject", tags = ["manual"])
# Testing rule.
provider_contents_test(name = "provider_contents_test",
target_under_test = ":provider_contents_subject")
# Note the target_under_test attribute is how the test rule depends on
# the real rule target.
# Entry point from the BUILD file; macro for running each test case's macro and
# declaring a test suite that wraps them together.
def myrules_test_suite(name):
# Call all test functions and wrap their targets in a suite.
_test_provider_co
ntents()
# ...
native.test_suite(
name = name,
tests = [
":provider_contents_test",
# ...
],
)
//mypkg/BUILD
:
load(":myrules.bzl", "myrule")
load(":myrules_test.bzl", "myrules_test_suite")
# Production use of the rule.
myrule(
name = "mytarget",
)
# Call a macro that defines targets that perform the tests at analysis time,
# and that can be executed with "bazel test" to r
eturn the result.
myrules_test_suite(name = "myrules_test")
可以使用 bazel test //mypkg:myrules_test
运行测试。
除了初始的 load()
语句之外,该文件还包含两个主要部分:
测试本身,每个测试都包含以下内容:1) 测试规则的分析时实现函数;2) 通过
analysistest.make()
声明测试规则;3) 用于声明被测规则(及其依赖项)和测试规则的加载时函数(宏)。如果断言在测试用例之间没有变化,则 1) 和 2) 可由多个测试用例共享。测试套件函数,用于为每个测试调用加载时函数,并声明一个将所有测试捆绑在一起的
test_suite
目标。
为保持一致性,请遵循建议的命名惯例:让 foo
代表测试名称中描述测试检查内容的这一部分(在上面的示例中为 provider_contents
)。例如,JUnit 测试方法可以命名为 testFoo
。
然后,执行以下操作:
生成测试和被测目标的宏应命名为
_test_foo
(_test_provider_contents
)其测试规则类型应命名为
foo_test
(provider_contents_test
)相应规则类型目标的标签应为
foo_test
(provider_contents_test
)测试规则的实现函数应命名为
_foo_test_impl
(_provider_contents_test_impl
)被测规则的目标及其依赖项的标签应以
foo_
(provider_contents_
) 为前缀
请注意,所有目标的标签可能会与同一 BUILD 软件包中的其他标签冲突,因此最好为测试使用唯一名称。
故障测试
验证规则在给定特定输入或处于特定状态时是否会失败可能很有用。您可以使用分析测试框架来完成此操作:
使用 analysistest.make
创建的测试规则应指定 expect_failure
:
failure_testing_test = analysistest.make(
_failure_testing_test_impl,
expect_failure = True,
)
测试规则实现应针对发生的失败的性质(具体而言,是失败消息)做出断言:
def _failure_testing_test_impl(ctx):
env = analysistest.begin(ctx)
asserts.expect_failure(env, "This rule should never work")
return analysistest
.end(env)
另请确保被测目标已明确标记为“手动”。
如果没有此属性,使用 :all
构建软件包中的所有目标会导致构建故意失败的目标,并显示构建失败。如果设置为“manual”,则只有在明确指定的情况下,或作为非手动目标的依赖项(例如您的测试规则)时,受测目标才会构建:
def _test_failure():
myrule(name = "this_should_fail", tags = ["manual"])
failure_testing_test(name = "failure_testing_test",
target_under_test = ":this_should_fail")
# Then call _test_failure() in the macro which generates the test suite and add
# ":failur
e_testing_test" to the suite's test targets.
验证已注册的操作
您可能需要编写测试,以断言规则注册的操作,例如使用 ctx.actions.run()
。您可以在分析测试规则实现函数中执行此操作。例如:
def _inspect_actions_test_impl(ctx):
env = analysistest.begin(ctx)
target_under_test = analysistest.target_under_test(env)
actions = analysistest.target_actions(env)
asserts.equals(env, 1, len(actions))
action_output = actions[0].outputs.to_list()[0]
asserts.equals(
env, target_under_test.label.name + ".out", action_output.basename)
return analysistest
.end(env)
请注意,analysistest.target_actions(env)
会返回一个 Action
对象列表,这些对象表示被测目标注册的操作。
验证不同标志下的规则行为
您可能希望验证实际规则在给定特定 build 标志的情况下是否以某种方式运行。例如,如果用户指定了以下内容,您的规则可能会有不同的行为:
bazel build //mypkg:real_target -c opt
vs.
bazel build //mypkg:real_target -c dbg
乍一看,这可以通过使用所需的 build 标志测试被测目标来实现:
bazel test //mypkg:myrules_test -c opt
但这样一来,您的测试套件就无法同时包含一个用于验证 -c opt
下的规则行为的测试,以及另一个用于验证 -c dbg
下的规则行为的测试。这两个测试无法在同一 build 中运行!
您可以在定义测试规则时指定所需的 build 标志,从而解决此问题:
myrule_c_opt_test = analysistest.make(
_myrule_c_opt_test_impl,
config_settings = {
"//command_line_option:compilation_mode": "
opt",
},
)
通常,系统会根据当前的 build 标志分析被测目标。
指定 config_settings
会覆盖指定命令行选项的值。(任何未指定的选项都将保留其在实际命令行中的值)。
在指定的 config_settings
字典中,命令行标志必须以特殊占位符值 //command_line_option:
为前缀,如上所示。
验证制品
检查生成的文件是否正确的主要方法如下:
您可以使用 Shell、Python 或其他语言编写测试脚本,并创建相应
*_test
规则类型的目标。您可以根据要执行的测试类型使用专门的规则。
使用测试目标平台
验证制品最直接的方法是编写脚本,并在 BUILD 文件中添加 *_test
目标。您要检查的具体制品应该是相应目标的数据依赖项。如果您的验证逻辑可用于多个测试,则应将其设为脚本,该脚本接受由测试目标的 args
属性控制的命令行实参。以下示例验证了上述 myrule
的输出是否为 "abc"
。
//mypkg/myrule_validator.sh
:
if [ "$(cat $1)" = "abc" ]; then
echo "Passed"
exit 0
els
e
echo "Failed"
exit 1
fi
//mypkg/BUILD
:
...
myrule(
name = "mytarget",
)
...
# Needed for each target whose artifacts are to be checked.
sh_test(
name = "validate_mytarget",
srcs = [":myrule_validator.sh"],
args = ["$(location :mytarget.out)&
quot;],
data = [":mytarget.out"],
)
使用自定义规则
一种更复杂的替代方法是将 shell 脚本编写为由新规则实例化的模板。这涉及更多间接寻址和 Starlark 逻辑,但可生成更简洁的 BUILD 文件。作为附带好处,任何实参预处理都可以在 Starlark 中完成,而不是在脚本中完成;由于脚本使用符号占位符(用于替换)而不是数字占位符(用于实参),因此脚本的自记录性略有提高。
//mypkg/myrule_validator.sh.template
:
if [ "$(cat %TARGET%)" = "abc" ]; then
echo "Passed"
exit 0
els
e
echo "Failed"
exit 1
fi
//mypkg/myrule_validation.bzl
:
def _myrule_validation_test_impl(ctx):
"""Rule for instantiating myrule_validator.sh.template for a given target."""
exe = ctx.outputs.executable
target = ctx.file.target
ctx.actions.expand_template(output = exe,
template = ctx.file._script,
is_executable = True,
substitutions = {
"%TARGET%": target.short_path,
})
# This is needed to make sure the output file of myrule is visible to the
# resulting instantiated script.
return [DefaultInfo(runfiles=ctx.runfiles(files=[target]))]
myrule_validation_test = rule(
implementation = _myrule_validation_test_impl,
attrs = {"target": attr.label(allow_single_file=True),
# You need an implicit dependency in order to access the template.
# A target could potentially override this attribute to modify
# the test logic.
"_script": attr.label(allow_single_file=True,
defa
ult=Label("//mypkg:myrule_validator"))},
test = True,
)
//mypkg/BUILD
:
...
myrule(
name = "mytarget",
)
...
# Needed just once, to expose the template. Could have also used export_files(),
# and made the _script attribute set allow_files=True.
filegroup(
name = "myrule_validator",
srcs = [":myrule_validator.sh.template"],
)
# Needed for each target whose artifacts are to be checked. Notice that you no
# longer have to specify the output file name in a data attribute, or its
# $(location) expansion in an args attribute, or the label for the script
# (unless you want to override it).
myrule_validation_test(
name = "validate_myta
rget",
target = ":mytarget",
)
或者,您也可以不使用模板扩展操作,而是将模板内嵌到 .bzl 文件中作为字符串,并在分析阶段使用 str.format
方法或 %
格式设置来扩展该字符串。
测试 Starlark 实用程序
Skylib 的 unittest.bzl
框架可用于测试实用函数(即既不是宏也不是规则实现的函数)。可以使用 unittest
,而不是使用 unittest.bzl
的 analysistest
库。对于此类测试套件,可以使用便捷函数 unittest.suite()
来减少样板代码。
//mypkg/myhelpers.bzl
:
def myhelper():
return "
abc"
//mypkg/myhelpers_test.bzl
:
load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
load(":myhelpers.bzl", "myhelper")
def _myhelper_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(env, "abc", myhelper())
return unittest.end(env)
myhelper_test = unittest.make(_myhelper_test_impl)
# No need for a test_myhelper() setup function.
def myhelpers_test_suite(name):
# unittest.suite() takes care of instantiating the testing rules and creating
# a test_suite.
unittest.suite(
name,
myhelper_test,
# ...
)
//mypkg/BUILD
:
load(":myhelpers_test.bzl", "myhelpers_test_suite")
myhelpers_test_suite(name =
"myhelpers_tests")
如需查看更多示例,请参阅 Skylib 的自有测试。