在 Bazel 中測試 Starlark 程式碼的方法有很多種。本頁面依用途彙整目前的最佳做法和架構。
測試規則
Skylib 提供名為 unittest.bzl
的測試架構,用於檢查規則的分析時間行為,例如動作和提供者。這類測試稱為「分析測試」,目前是測試規則內部運作方式的最佳選項。
以下列出一些注意事項:
測試斷言會在建構程序中發生,而非在獨立的測試執行程式程序中發生。由測試建立的目標必須命名為不與其他測試或建構作業的目標衝突。Bazel 會將測試期間發生的錯誤視為建構中斷,而非測試失敗。
您需要大量的樣板,才能設定測試中的規則,以及包含測試斷言的規則。這個範本一開始可能會讓人卻步,請記住,系統會在載入階段評估巨集並產生目標,而規則實作函式則會在稍後的分析階段執行。
分析測試的目的在於提供較小且輕量化的測試。分析測試架構的部分功能僅限於驗證目標,且其間接依附元件的數量上限為 500 (目前為 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:
,如上方所示。
驗證構件
檢查產生的檔案是否正確的主要方法如下:
您可以使用 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
,而非 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 自己的測試。