Pengujian

Laporkan masalah Lihat sumber Per Malam · 7,3 · 7,2 · 7,1 · 7,0 · 6,5

Ada beberapa pendekatan untuk menguji kode Starlark di Bazel. Ini mengumpulkan praktik terbaik dan kerangka kerja terkini berdasarkan kasus penggunaan.

Aturan pengujian

Skylib memiliki framework pengujian yang disebut unittest.bzl untuk memeriksa perilaku aturan waktu analisis, seperti tindakan dan penyedia layanan. Pengujian semacam itu disebut "pengujian analisis" dan saat ini yang terbaik pilihan untuk menguji cara kerja aturan internal.

Beberapa hal yang perlu diwaspadai:

  • Pernyataan pengujian terjadi dalam build, bukan proses runner pengujian terpisah. Target yang dibuat oleh pengujian harus diberi nama sedemikian rupa sehingga tidak bertabrakan dengan target dari pengujian lain atau dari build. Sebuah {i>error<i} yang selama pengujian dilihat oleh Bazel sebagai kerusakan build, bukan kegagalan uji.

  • Diperlukan jumlah boilerplate yang cukup untuk menyiapkan aturan yang sedang diuji dan aturan yang berisi pernyataan pengujian. Boilerplate ini mungkin tampak sulit terlebih dahulu. Sebaiknya ingat bahwa makro dievaluasi dan target yang dibuat selama fase pemuatan, sedangkan fungsi implementasi tidak berjalan sampai nanti, yaitu selama fase analisis.

  • Pengujian analisis dimaksudkan agar berukuran cukup kecil dan ringan. Tertentu fitur kerangka kerja pengujian analisis dibatasi untuk memverifikasi target dengan jumlah maksimum dependensi transitif (saat ini 500). Hal ini karena implikasi performa dari penggunaan fitur ini dengan pengujian.

Prinsip dasarnya adalah mendefinisikan aturan pengujian yang bergantung pada aturan-yang sedang diuji. Tindakan ini memberi akses aturan pengujian ke aturan penyedia layanan.

Fungsi implementasi aturan pengujian menjalankan pernyataan. Jika ada kegagalan apa pun, peristiwa ini tidak akan langsung dimunculkan dengan memanggil fail() (yang akan akan memicu error build waktu analisis), melainkan dengan menyimpan error dalam skrip yang dihasilkan yang gagal pada waktu eksekusi uji.

Lihat contoh mainan minimal di bawah, diikuti dengan contoh yang memeriksa tindakan.

Contoh minimal

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

Pengujian dapat dijalankan dengan bazel test //mypkg:myrules_test.

Selain pernyataan load() awal, ada dua bagian utama dari file:

  • Pengujian itu sendiri, yang masing-masing terdiri dari 1) analisis waktu untuk aturan pengujian, 2) deklarasi fungsi aturan pengujian melalui analysistest.make(), dan 3) fungsi waktu pemuatan (makro) untuk mendeklarasikan aturan-yang-sedang-diuji (dan dependensinya) dan pengujian aturan. Jika pernyataan tidak berubah antara kasus pengujian, 1) dan 2) mungkin dibagikan oleh beberapa kasus pengujian.

  • Fungsi rangkaian pengujian, yang memanggil fungsi waktu pemuatan untuk setiap menguji, dan mendeklarasikan target test_suite yang menggabungkan semua pengujian.

Agar konsisten, ikuti konvensi penamaan yang direkomendasikan: Biarkan foo merupakan singkatan dari bagian dari nama pengujian yang mendeskripsikan apa yang diperiksa oleh pengujian (provider_contents dalam contoh di atas). Misalnya, metode pengujian JUnit akan diberi nama testFoo.

Lalu:

  • makro yang membuat pengujian dan target yang sedang diuji seharusnya bernama _test_foo (_test_provider_contents)

  • jenis aturan pengujiannya harus bernama foo_test (provider_contents_test)

  • label target jenis aturan ini harus foo_test (provider_contents_test)

  • fungsi implementasi untuk aturan pengujian harus diberi nama _foo_test_impl (_provider_contents_test_impl)

  • label target dari aturan yang sedang diuji serta dependensinya harus diawali dengan foo_ (provider_contents_)

Perhatikan bahwa label semua target dapat bertentangan dengan label lain yang BUILD, jadi sebaiknya gunakan nama yang unik untuk pengujian.

Pengujian kegagalan

Ada baiknya Anda memverifikasi bahwa aturan gagal saat diberikan input tertentu atau dalam status. Hal ini dapat dilakukan menggunakan kerangka kerja pengujian analisis:

Aturan pengujian yang dibuat dengan analysistest.make harus menentukan expect_failure:

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

Implementasi aturan pengujian harus membuat pernyataan tentang sifat kegagalan yang terjadi (khususnya, pesan kegagalan):

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

Pastikan juga target yang sedang diuji diberi tag 'manual' secara khusus. Tanpanya, mem-build semua target dalam paket Anda menggunakan :all akan menghasilkan target yang sengaja gagal dan akan menampilkan kegagalan build. Dengan 'manual', target Anda yang diuji hanya akan dibuat jika ditetapkan secara eksplisit, atau jika dependensi target non-manual (seperti aturan pengujian Anda):

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.

Memverifikasi tindakan yang terdaftar

Anda mungkin ingin menulis pengujian yang membuat pernyataan tentang tindakan yang didaftarkan, misalnya, menggunakan ctx.actions.run(). Ini dapat dilakukan di analisis fungsi implementasi aturan pengujian. Misalnya:

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)

Perhatikan bahwa analysistest.target_actions(env) menampilkan daftar Objek Action yang mewakili tindakan yang didaftarkan oleh target yang sedang diuji.

Memverifikasi perilaku aturan pada tanda yang berbeda

Anda mungkin ingin memverifikasi bahwa aturan yang sebenarnya berperilaku dengan cara tertentu berdasarkan build tertentu penanda. Misalnya, aturan Anda mungkin berperilaku berbeda jika pengguna menentukan:

bazel build //mypkg:real_target -c opt

versus

bazel build //mypkg:real_target -c dbg

Sekilas, hal ini dapat dilakukan dengan menguji target yang sedang diuji menggunakan flag build yang diinginkan:

bazel test //mypkg:myrules_test -c opt

Namun, tidak mungkin rangkaian pengujian Anda berisi pengujian yang memverifikasi perilaku aturan berdasarkan -c opt dan pengujian lain yang memverifikasi perilaku aturan pada -c dbg. Kedua pengujian tidak dapat dijalankan dalam bangunan yang sama!

Hal ini dapat diatasi dengan menentukan flag build yang diinginkan saat menentukan pengujian aturan:

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

Biasanya, target yang sedang diuji dianalisis mengingat flag build saat ini. Menentukan config_settings akan menggantikan nilai command line yang ditentukan lainnya. (Opsi yang tidak ditentukan akan mempertahankan nilainya dari opsi command line).

Dalam kamus config_settings yang ditentukan, tanda command line harus berupa diawali dengan nilai placeholder khusus //command_line_option:, seperti yang ditunjukkan di atas.

Memvalidasi artefak

Cara utama untuk memeriksa apakah file yang Anda hasilkan sudah benar adalah:

  • Anda dapat menulis skrip pengujian di {i>shell<i}, Python, atau bahasa lain, dan buat target dari jenis aturan *_test yang sesuai.

  • Anda dapat menggunakan aturan khusus untuk jenis pengujian yang ingin dilakukan.

Menggunakan target pengujian

Cara paling mudah untuk memvalidasi artefak adalah dengan menulis skrip dan tambahkan target *_test ke file BUILD Anda. Artefak spesifik yang ingin Anda pemeriksaan harus berupa dependensi data dari target ini. Jika logika validasi Anda dapat digunakan kembali untuk beberapa pengujian, skrip itu harus berupa skrip yang menggunakan command line argumen yang dikontrol oleh atribut args target pengujian. Berikut adalah contoh yang memvalidasi bahwa output myrule dari atas adalah "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"],
)

Menggunakan aturan khusus

Alternatif yang lebih rumit adalah dengan menulis skrip {i>shell<i} sebagai {i>template<i} yang dibuat instance-nya oleh aturan baru. Hal ini melibatkan lebih banyak arah dan Starlark logika, tetapi mengarah ke file BUILD yang lebih bersih. Sebagai manfaat tambahan, argumen apa pun pra-pemrosesan dapat dilakukan di Starlark alih-alih di skrip, dan skrip sedikit lebih mendokumentasikan diri sendiri karena menggunakan {i>placeholder<i} simbolis (untuk substitusi) bukan angka (untuk argumen).

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

Atau, alih-alih menggunakan tindakan perluasan {i>template<i}, Anda dapat menyisipkan template ke dalam file .bzl sebagai string dan memperluasnya selama fase analisis menggunakan metode str.format atau pemformatan %.

Menguji utilitas Starlark

Skylib unittest.bzl dapat digunakan untuk menguji fungsi utilitas (yaitu, fungsi yang makro maupun penerapan aturan). Daripada menggunakan unittest.bzl Library analysistest, unittest mungkin digunakan. Untuk rangkaian pengujian semacam itu, fungsi praktis unittest.suite() dapat digunakan untuk mengurangi boilerplate.

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

Untuk contoh lainnya, lihat pengujian Skylib sendiri.