Ada beberapa pendekatan untuk menguji kode Starlark di Bazel. Halaman ini mengumpulkan praktik terbaik dan framework saat ini berdasarkan kasus penggunaan.
Aturan pengujian
Skylib memiliki framework pengujian yang disebut unittest.bzl
untuk memeriksa perilaku waktu analisis aturan, seperti tindakan dan penyedianya. Pengujian tersebut disebut "pengujian analisis" dan saat ini merupakan opsi terbaik untuk menguji cara kerja internal aturan.
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 agar tidak bertabrakan dengan target dari pengujian lain atau dari build. Error yang terjadi selama pengujian akan dilihat oleh Bazel sebagai kerusakan build, bukan kegagalan pengujian.
Diperlukan jumlah boilerplate yang cukup untuk menyiapkan aturan yang sedang diuji dan aturan yang berisi pernyataan pengujian. Boilerplate ini mungkin tampak sulit pada awalnya. Ada baiknya ingat bahwa makro dievaluasi dan target yang dibuat selama fase pemuatan, sedangkan fungsi penerapan aturan tidak berjalan sampai nanti, selama fase analisis.
Pengujian analisis dimaksudkan agar berukuran cukup kecil dan ringan. Fitur tertentu dari framework pengujian analisis dibatasi untuk memverifikasi target dengan jumlah dependensi transitif maksimum (saat ini 500). Hal ini disebabkan oleh implikasi performa dari penggunaan fitur ini dengan pengujian yang lebih besar.
Prinsip dasarnya adalah menentukan aturan pengujian yang bergantung pada aturan yang sedang diuji. Tindakan ini akan memberikan akses kepada aturan pengujian ke penyedia aturan yang sedang diuji.
Fungsi implementasi aturan pengujian menjalankan pernyataan. Jika terjadi
kegagalan, kegagalan tersebut tidak segera ditampilkan dengan memanggil fail()
(yang 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) fungsi implementasi waktu analisis untuk aturan pengujian, 2) deklarasi aturan pengujian melalui
analysistest.make()
, dan 3) fungsi waktu pemuatan (makro) untuk mendeklarasikan aturan yang sedang diuji (dan dependensinya) serta aturan pengujian. Jika pernyataan tidak berubah antara kasus pengujian, 1) dan 2) dapat digunakan bersama oleh beberapa kasus pengujian.Fungsi rangkaian pengujian, yang memanggil fungsi waktu pemuatan untuk setiap pengujian, dan mendeklarasikan target
test_suite
yang memaketkan semua pengujian bersama-sama.
Untuk konsistensi, ikuti konvensi penamaan yang direkomendasikan: Biarkan foo
merupakan singkatan dari
bagian dari nama pengujian yang mendeskripsikan hal yang diperiksa pengujian
(provider_contents
dalam contoh di atas). Misalnya, metode pengujian JUnit
akan diberi nama testFoo
.
Lalu:
makro yang menghasilkan pengujian dan target yang sedang diuji harus dinamai
_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 penerapan untuk aturan pengujian harus diberi nama
_foo_test_impl
(_provider_contents_test_impl
)label target aturan yang sedang diuji dan dependensinya harus diawali dengan
foo_
(provider_contents_
)
Perlu diketahui bahwa label semua target dapat bertentangan dengan label lain dalam paket BUILD yang sama, jadi sebaiknya gunakan nama yang unik untuk pengujian.
Pengujian kegagalan
Sebaiknya Anda memverifikasi bahwa aturan gagal dengan input tertentu atau dalam status tertentu. 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 mengakibatkan
build target yang sengaja gagal dan akan menampilkan kegagalan build. Dengan
'manual', target Anda yang sedang diuji hanya akan dibuat jika ditentukan secara eksplisit, atau
sebagai 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 aturan Anda, misalnya, menggunakan ctx.actions.run()
. Hal ini dapat dilakukan dalam
fungsi penerapan aturan pengujian analisis. 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 aturan yang sebenarnya berperilaku dengan cara tertentu mengingat tanda build tertentu. Misalnya, aturan Anda mungkin berperilaku berbeda jika pengguna menentukan:
bazel build //mypkg:real_target -c opt
versus
bazel build //mypkg:real_target -c dbg
Secara 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 secara bersamaan yang memverifikasi perilaku aturan dalam -c opt
dan pengujian lain yang memverifikasi perilaku aturan dalam -c dbg
. Kedua pengujian tidak akan dapat dijalankan
di build yang sama.
Hal ini dapat diatasi dengan menentukan flag build yang diinginkan saat menentukan aturan pengujian:
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 mengganti nilai opsi command line
yang ditentukan. (Opsi apa pun yang tidak ditentukan akan mempertahankan nilainya dari command line
yang sebenarnya).
Dalam kamus config_settings
yang ditentukan, tanda command line harus
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 shell, Python, atau bahasa lain, dan membuat 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 menambahkan target *_test
ke file BUILD Anda. Artefak spesifik yang ingin Anda
periksa harus berupa dependensi data target ini. Jika logika validasi Anda
dapat digunakan kembali untuk beberapa pengujian, logika tersebut harus berupa skrip yang menerima argumen command line
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 shell sebagai template yang dibuat instance-nya oleh aturan baru. Hal ini melibatkan lebih banyak logika Starlark dan tidak langsung, tetapi menghasilkan file BUILD yang lebih bersih. Sebagai manfaat sampingan, prapemrosesan argumen apa pun dapat dilakukan di Starlark, bukan di skrip, dan skrip ini sedikit mendokumentasikan mandiri karena menggunakan placeholder simbolis (untuk substitusi), bukan di skrip numerik (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, daripada menggunakan tindakan perluasan template, Anda dapat
menyejajarkan template ke dalam file .bzl sebagai string dan memperluasnya selama
fase analisis menggunakan metode str.format
atau pemformatan %
.
Menguji utilitas Starlark
Framework unittest.bzl
dari Skylib dapat digunakan untuk menguji fungsi utilitas (yaitu fungsi yang bukan makro atau implementasi aturan). unittest
dapat digunakan, bukan menggunakan library
analysistest
unittest.bzl
. 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.