測試

回報問題 查看來源

在 Bazel 中測試 Starlark 程式碼有幾種不同的方法。本頁面依用途收集目前的最佳做法和架構。

測試規則

Skylib 具有名為 unittest.bzl 的測試架構,可檢查規則的分析時間行為,例如規則的動作和提供者。這類測試稱為「分析測試」,目前最適合用來測試規則的內部運作。

注意事項:

  • 測試斷言會在建構作業中進行,而非獨立的測試執行器程序。您必須為測試建立的目標命名,確保目標不會與其他測試或版本的目標衝突。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) 透過 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 建構所有目標將導致建構失敗的目標,並會顯示建構失敗。如果採用「手動」,則只有在明確指定或做為非手動目標 (例如測試規則) 的依附元件時,系統才會建構受測試的目標:

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

乍看之下,您可以使用所需建構標記測試要測試的目標:

bazel test //mypkg:myrules_test -c opt

但是,測試套件無法同時包含測試,用來驗證 -c opt 底下的規則行為,以及另一項測試來驗證 -c dbg 下的規則行為。這兩種測試無法在同一個版本中執行!

如要解決這個問題,請在定義測試規則時指定所需的建構標記:

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:,如上所示。

驗證構件

檢查產生的檔案是否正確的主要方法包括:

  • 您可以使用殼層、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"],
)

使用自訂規則

另一個較複雜的替代方案是將殼層指令碼編寫為範本,並讓新規則執行個體化。這涉及更多間接邏輯和 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,而不是 unittest.bzlanalysistest 程式庫。針對這類測試套件,便利函式 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 本身的測試