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()
ステートメントを除き、ファイルには主に次の 2 つの部分があります。
テスト自体は、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)
また、テスト対象のターゲットに「manual」というタグが付けられていることを確認してください。これがないと、: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
一見すると、これは目的のビルドフラグを使用してテスト対象をテストすることで実現できます。
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
ルールタイプのターゲットを作成できます。実行するテストの種類に応じて、専用のルールを使用できます。
テスト ターゲットを使用する
アーティファクトを検証する最も簡単な方法は、スクリプトを作成して、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"],
)
カスタムルールを使用する
より複雑な代替手段として、新しいルールによってインスタンス化されるテンプレートとしてシェル スクリプトを作成する方法があります。間接参照と 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 独自のテストをご覧ください。