2022 年 BazelCon 将于 11 月 16 日至 17 日在纽约和线上举办。
立即报名!

测试

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

在 Bazel 中测试 Starlark 代码的方法有多种。本页按用例收集了当前的最佳做法和框架。

测试规则

Skylib 有一个名为 unittest.bzl 的测试框架,用于检查规则的分析时行为,如其操作和提供商。此类测试称为“分析测试”,目前是测试规则内部工作原理的最佳选择。

一些注意事项:

  • 测试断言发生在 build 中,不是单独的测试运行程序进程。由测试创建的目标必须进行命名,这样它们就不会与其他测试或构建中的目标发生冲突。Bazel 测试期间发生的错误被视为 build 中断,而不是测试失败。

  • 需要相当数量的样板代码来设置被测规则和包含测试断言的规则。这种样板代码 first 看起来可能会令人望而生.。请注意,宏要在加载阶段进行评估和生成,而规则实现函数要等到分析阶段结束后才会运行。

  • 分析测试相当小巧。分析测试框架的某些功能仅限于验证具有最大传递依赖项(目前为 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) 通过 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
# ":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 对象的列表,这些对象表示受测目标注册的操作。

验证不同标志下的规则行为

您可能希望验证某些基于特定构建标志的实际规则的行为。例如,如果用户指定了以下内容,您的规则的行为可能会有所不同:

bazel build //mypkg:real_target -c opt

对比

bazel build //mypkg:real_target -c dbg

At 一看,您可以使用所需的构建标记测试受测目标:

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",
    },
)

通常,如果当前构建标志有问题,系统会分析受测目标。指定 config_settings 会替换指定命令行选项的值。(任何未指定的选项都将保留其在实际命令行中的值)。

在指定 config_settings 字典中,命令行标志必须以特殊占位符值 //command_line_option: 为前缀,如上所示。

验证工件

检查生成的文件是否正确的主要方法如下:

  • 您可以使用 shell、Python 或其他语言编写测试脚本,并创建相应 *_test 规则类型的目标。

  • 您可以针对要执行的测试类型使用专用规则。

使用测试目标

验证工件的最简单方法是编写脚本并将 *_test 目标添加到 BUILD 文件。您要检查的特定工件应该是此目标的数据依赖项。如果您的验证逻辑可重复用于多个测试,那么该脚本应该采用受测试目标的 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 实用程序

Skylibunittest.bzl 框架可用于测试效用函数(即既不是宏也不是规则实现的函数) )。不使用unittest.bzlanalysistestunittest。对于此类测试套件,使用便捷函数 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 自己的测试