BazelCon 2022 findet vom 16. bis 17. November in New York und online statt.
Jetzt anmelden

Test

Mit Sammlungen den Überblick behalten Sie können Inhalte basierend auf Ihren Einstellungen speichern und kategorisieren.

Es gibt verschiedene Ansätze zum Testen von Starlark-Code in Bazel. Auf dieser Seite werden die aktuellen Best Practices und Frameworks nach Anwendungsfall zusammengefasst.

Regeln testen

Skylib hat ein Test-Framework namens unittest.bzl, mit dem das Verhalten der Regeln während der Analyse wie das Verhalten und Anbieter überprüft wird. Diese Tests werden als "Analysetests" bezeichnet und sind derzeit die beste Option zum Testen des Inneren von Regeln.

Beachten Sie Folgendes:

  • TestAssertions finden Sie im Build, nicht in einem separaten Test-Ausführer-Prozess. Ziele, die durch den Test erstellt werden, müssen so benannt werden, dass sie nicht mit Zielen aus anderen Tests oder aus dem Build kollidieren. Ein Fehler, der während des Tests auftritt, wird von Bazel als Build-Fehler und nicht als Testfehler betrachtet.

  • Die Einrichtung der Regeln, die getestet werden sollen, und der Regeln, die Test-Assertions enthalten, erfordert eine Menge Standardregeln. Dieser Boilerplate-Code kann auf den ersten Blick ziemlich abschreckend wirken. Beachten Sie, dass die Makros ausgewertet und die Ziele während der Ladephase generiert werden. Funktionen zur Regelimplementierung werden erst später in der Analysephase ausgeführt.

  • Analysetests sind ziemlich klein und einfach. Bestimmte Features des Analyse-Testframeworks sind auf die Prüfung von Zielen mit einer maximalen Anzahl von transitiven Abhängigkeiten (derzeit 500) beschränkt. Dies ist auf die Leistung zurückzuführen, die sich aus der Verwendung dieser Features bei größeren Tests ergibt.

Das Prinzip besteht darin, eine Testregel zu definieren, die von der zu testenden Regel abhängt. So erhält die Testregel Zugriff auf die Anbieter des Regel-Test-Anbieters.

Die Implementierungsfunktion der Testregel führt Assertionen aus. Wenn Fehler auftreten, werden sie nicht sofort durch Aufrufen von fail() ausgelöst (was einen Build-Fehler bei der Analyse auslösen würde), sondern vielmehr durch Speichern der Fehler in einem generierten Skript, bei dem Fehler auftreten. zum Zeitpunkt der Testausführung.

Unten findest du ein einfaches Spielzeugbeispiel, gefolgt von einem Beispiel, mit dem Aktionen geprüft werden.

Minimales Beispiel

//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")

Der Test kann mit bazel test //mypkg:myrules_test ausgeführt werden.

Abgesehen von den ursprünglichen load()-Anweisungen gibt es zwei Hauptteile in der Datei:

  • Die Tests selbst, die jeweils aus einer 1) Analysezeit-Implementierungsfunktion für die Testregel, 2) einer Deklaration der Testregel über analysistest.make() und 3) einer Ladezeitfunktion bestehen. (Makro) zum Deklarieren der zu testenden Regel und ihrer Abhängigkeiten sowie zum Testen der Regel. Wenn sich die Assertions zwischen Testläufen nicht ändern, können 1) und 2) von mehreren Testläufen gemeinsam genutzt werden.

  • Die Testsuite-Funktion, die die Ladezeit-Funktionen für jeden Test aufruft und ein test_suite-Ziel deklariert, das alle Tests zusammenfasst.

Halten Sie sich aus Konsistenzgründen an die empfohlene Namenskonvention: foo steht für den Teil des Testnamens, der beschreibt, was der Test überprüft (provider_contents im Beispiel oben). Eine JUnit-Testmethode würde beispielsweise testFoo heißen.

Dann:

  • Das Makro, das den Test und das zu testende Ziel generiert, sollte _test_foo (_test_provider_contents) heißen.

  • Der Testregeltyp sollte foo_test (provider_contents_test) lauten

  • Das Label des Ziels dieses Regeltyps sollte foo_test (provider_contents_test) sein.

  • sollte die Implementierungsfunktion für die Testregel _foo_test_impl (_provider_contents_test_impl) heißen.

  • Den Labels der Ziele der zu testenden Regeln und ihrer Abhängigkeiten sollte das Präfix foo_ (provider_contents_) vorangestellt werden

Die Labels aller Ziele können mit anderen Labels im selben BUILD-Paket in Konflikt stehen. Daher ist es sinnvoll, für den Test einen eindeutigen Namen zu verwenden.

Fehlertests

Es kann hilfreich sein, zu prüfen, ob eine Regel aufgrund bestimmter Eingaben oder in einem bestimmten Status fehlschlägt. Dazu verwenden Sie das Analysetest-Framework:

Die mit analysistest.make erstellte Testregel sollte expect_failure angeben:

failure_testing_test = analysistest.make(
    _failure_testing_test_impl,
    expect_failure = True,
)

Die Implementierung der Testregel sollte die Art des aufgetretenen Fehlers bestätigen (insbesondere die Fehlermeldung):

def _failure_testing_test_impl(ctx):
    env = analysistest.begin(ctx)
    asserts.expect_failure(env, "This rule should never work")
    return analysistest.end(env)

Achten Sie auch darauf, dass das zu testende Ziel explizit mit dem Tag "manual" gekennzeichnet ist. Andernfalls würde die Erstellung aller Ziele in Ihrem Paket mit :all zu einem Build des absichtlich ausfallenden Ziels und zu einem Build-Fehler führen. Bei der Option "Manuell" erstellt das zu testende Ziel nur dann, wenn es explizit angegeben wird oder als Abhängigkeit eines nicht manuellen Ziels (z. B. Ihrer Testregel):

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.

Registrierte Aktionen prüfen

Sie können auch Tests schreiben, mit denen Informationen zu den Aktionen gespeichert werden, die von der Regel registriert werden, z. B. mit ctx.actions.run(). Dies kann mit der Funktion zum Implementieren der Analysetestregel durchgeführt werden. Beispiel:

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)

Beachten Sie, dass analysistest.target_actions(env) eine Liste von Action-Objekten zurückgibt, die Aktionen darstellen, die vom zu testenden Ziel registriert sind.

Regelverhalten unter verschiedenen Flags prüfen

Unter Umständen möchten Sie prüfen, ob sich Ihre echte Regel anhand bestimmter Build-Flags verhält. Beispielsweise verhält sich Ihre Regel möglicherweise anders, wenn ein Nutzer Folgendes angibt:

bazel build //mypkg:real_target -c opt

im Vergleich mit

bazel build //mypkg:real_target -c dbg

Dazu können Sie auf den ersten Blick das zu testende Ziel mit den gewünschten Build-Flags testen:

bazel test //mypkg:myrules_test -c opt

Anschließend kann Ihre Testsuite jedoch nicht mehr gleichzeitig einen Test enthalten, der das Regelverhalten unter -c opt und einen weiteren Test verifiziert, der das Regelverhalten unter -c dbg bestätigt. Beide Tests lassen sich nicht im selben Build ausführen.

Dieses Problem lässt sich beheben, indem Sie beim Definieren der Testregel die gewünschten Build-Flags angeben:

myrule_c_opt_test = analysistest.make(
    _myrule_c_opt_test_impl,
    config_settings = {
        "//command_line_option:compilation_mode": "opt",
    },
)

Normalerweise wird ein zu testendes Ziel anhand der aktuellen Build-Flags analysiert. Durch die Angabe von config_settings werden die Werte der angegebenen Befehlszeilenoptionen überschrieben. Bei allen nicht festgelegten Optionen werden die Werte aus der tatsächlichen Befehlszeile übernommen.

Im angegebenen config_settings-Wörterbuch muss den Befehlszeilen-Flags der spezielle Platzhalterwert //command_line_option: vorangestellt werden, wie oben gezeigt.

Artefakte prüfen

Anhand der folgenden Methoden können Sie prüfen, ob Ihre generierten Dateien korrekt sind:

  • Sie können ein Testskript in Shell, Python oder einer anderen Sprache schreiben und ein Ziel des entsprechenden Regeltyps *_test erstellen.

  • Sie können für die Art des Tests, den Sie ausführen möchten, eine spezielle Regel verwenden.

Testziel verwenden

Die einfachste Möglichkeit, ein Artefakt zu validieren, besteht darin, ein Skript zu schreiben und Ihrer BUILD-Datei ein *_test-Ziel hinzuzufügen. Die spezifischen Artefakte, die Sie prüfen möchten, sollten Datenabhängigkeiten dieses Ziels sein. Wenn Ihre Validierungslogik für mehrere Tests wiederverwendbar ist, sollte es ein Skript sein, das Befehlszeilenargumente verwendet, die durch das Attribut args des Testziels gesteuert werden. In diesem Beispiel wird geprüft, ob die Ausgabe von myrule oben den Wert "abc" hat.

//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"],
)

Benutzerdefinierte Regel verwenden

Eine komplexere Alternative besteht darin, das Shell-Skript als Vorlage zu schreiben, die von einer neuen Regel instanziiert wird. Dies führt zu mehr Indirektion und Starlark-Logik, führt jedoch zu saubereren BUILD-Dateien. Als Vorzug kann jede Argumentvorverarbeitung in Starlark anstelle des Skripts ausgeführt werden. Das Skript ist etwas dokumentarischer, da es symbolische Platzhalter (für Substitutionen) anstelle von numerischen Platzhaltern verwendet. (für Argumente).

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

Statt eine Vorlagenerweiterungsaktion zu verwenden, können Sie die Vorlage auch als String in die BZL-Datei einfügen und während der Analysephase mit der Methode str.format oder % erweitern. .

Starlark-Dienstprogramme testen

Mit dem unittest.bzl-Framework von Skylib können Dienstprogrammfunktionen getestet werden (d. h. Funktionen, bei denen es sich weder um Makros noch um Regelimplementierungen handelt) ). Anstelle der analysistest-Bibliothek von unittest.bzl kann unittest verwendet werden. Für solche Testsuiten kann die praktische Funktion unittest.suite() verwendet werden, um den Standardcode zu reduzieren.

//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")

Weitere Beispiele finden Sie in den Tests von Skylib.