您可以使用几种不同的方法在 Bazel 中测试 Starlark 代码。本次 页面按用例收集了当前的最佳做法和框架。
测试规则
Skylib 有一个名为
unittest.bzl
用于检查规则的分析时行为,
提供商。此类测试称为“分析测试”它们是目前
用于测试规则内部运作情况的选项。
一些注意事项:
测试断言发生在 build 中,而不是单独的测试运行程序进程中。 测试创建的目标时,必须将其命名 与其他测试或 build 中的目标冲突。导致 就会被 Bazel 视为构建中断,而不是 测试失败。
它需要大量的样板文件来设置正在测试的规则, 包含测试断言的规则此样板在 。请务必注意 并在加载阶段生成目标,而规则 之后,在分析阶段才会运行。
分析测试旨在进行非常小的轻量级测试。特定 分析测试框架的功能仅限于验证 具有最大传递依赖项数(目前为 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_contents()
# ...
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 return the result.
myrules_test_suite(name = "myrules_test")
您可以使用 bazel test //mypkg:myrules_test
运行测试。
除了最初的 load()
语句之外,
文件:
测试本身,每个测试都包括 1) 分析时 实现函数;2) 声明 以及 3) 加载时间函数
analysistest.make()
(宏)用于声明被测规则(及其依赖项)并进行测试 规则。如果断言在测试用例之间没有变化,则 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_
)
请注意,所有目标的标签可能会与同一 构建软件包,因此最好为测试使用唯一的名称。
故障测试
根据某些输入或特定条件,验证规则是否失败可能会非常有用 状态。这可以通过分析测试框架来完成:
使用 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
构建软件包中的所有目标会导致
build 会导致构建失败。包含
'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
# ":failure_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
乍一看,这可以通过使用 所需的构建标志:
bazel test //mypkg:myrules_test -c opt
但这样,您的测试套件就不可能同时包含
另一个测试用于验证 -c opt
下的规则行为,另一个测试用于
用于验证 -c dbg
下的规则行为。这两项测试都将无法运行
同一版本!
这可以通过在定义测试时指定所需的 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
字典中,命令行 flag 必须为
带有一个特殊占位符值 //command_line_option:
前缀,如下所示
。
验证制品
检查生成的文件是否正确的主要方法是:
您可以使用 shell、Python 或其他语言编写测试脚本,并且 然后创建适当的
*_test
规则类型的目标。您可以针对要执行的测试类型使用专门的规则。
使用测试目标
验证工件的最直接方法是编写脚本,
向 BUILD 文件添加 *_test
目标。您要创建的特定工件
应该是此目标的数据依赖项。如果您的验证逻辑是
可在多个测试中重复使用,它应该是接受命令行的脚本
由测试目标的 args
属性控制的参数。这里有
此示例,用于验证上述 myrule
的输出是否为 "abc"
。
//mypkg/myrule_validator.sh
:
if [ "$(cat $1)" = "abc" ]; then
echo "Passed"
exit 0
else
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)"],
data = [":mytarget.out"],
)
使用自定义规则
更复杂的替代方案是将 Shell 脚本编写为模板, 被新规则实例化这涉及更多间接方法和 Starlark, 逻辑,但会提供更简洁的 BUILD 文件。附带好处: 预处理工作可以在 Starlark 中完成,而不是在脚本中完成,并且脚本 由于它使用符号占位符(例如, 替换),而不是数字(用于参数)。
//mypkg/myrule_validator.sh.template
:
if [ "$(cat %TARGET%)" = "abc" ]; then
echo "Passed"
exit 0
else
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,
default=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_mytarget",
target = ":mytarget",
)
或者,您也可以不使用模板展开操作
将模板作为字符串内联到 .bzl 文件中,并在
使用 str.format
方法或 %
格式设置来分析阶段。
测试 Starlark 实用程序
Skylib
unittest.bzl
框架可用于测试实用函数(即
既不是宏,也不是规则实施)。不使用 unittest.bzl
的
analysistest
库,可以使用 unittest
。对于此类测试套件,
便捷函数 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 自己的测试。