テスト

問題を報告する ソースを表示 ナイトリー · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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_testprovider_contents_test)にする必要があります。

  • このルールタイプのターゲットのラベルは foo_testprovider_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 を使用してパッケージ内のすべてのターゲットをビルドすると、意図的に失敗するターゲットがビルドされ、ビルドエラーが発生します。「手動」の場合、テスト対象のターゲットは、明示的に指定された場合、または手動以外のターゲット(テストルールなど)の依存関係としてのみビルドされます。

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 ユーティリティのテスト

Skylibunittest.bzl フレームワークを使用すると、ユーティリティ関数(マクロでもルール実装でもない関数)をテストできます。unittest.bzlanalysistest ライブラリを使用する代わりに、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 独自のテストをご覧ください。