Starlark kodunu Bazel'de test etmek için çeşitli yaklaşımlar vardır. Bu sayfada, kullanım alanına göre mevcut en iyi uygulamalar ve çerçeveler toplanmıştır.
Kuralları test etme
Skylib'te kuralların analiz zamanındaki davranışlarını (ör. eylemleri ve sağlayıcıları) kontrol etmek için unittest.bzl
adlı bir test çerçevesi bulunur. Bu tür testler "analiz testleri" olarak adlandırılır ve şu anda kuralların iç işleyişini test etmek için en iyi seçenektir.
Dikkat edilmesi gereken bazı noktalar:
Test onayları derleme içinde gerçekleşir, ayrı bir test çalıştırıcı işleminde değil. Test tarafından oluşturulan hedefler, diğer testlerdeki veya derlemedeki hedeflerle çakışmayacak şekilde adlandırılmalıdır. Test sırasında oluşan bir hata Bazel tarafından test hatası yerine bir derleme kesintisi olarak algılanır.
Test edilen kuralları ve test onaylarını içeren kuralları oluşturmak için yeterli miktarda ortak metin gerekir. Bu standart, başta göz korkutucu görünebilir. Makroların değerlendirildiği ve hedeflerin yükleme aşamasında oluşturulduğu, ancak kural uygulama işlevlerinin analiz aşamasında ileri bir aşamaya kadar çalışmayacağını unutmayın.
Analiz testlerinin amacı oldukça küçük ve hafiftir. Analiz test çerçevesinin belirli özellikleri, maksimum sayıda geçişli bağımlılığa (şu anda 500) sahip hedeflerin doğrulanmasıyla sınırlıdır. Bunun nedeni, bu özelliklerin daha kapsamlı testlerle kullanılmasının performans üzerindeki etkileridir.
Temel ilke, test altındaki kurala bağlı bir test kuralı tanımlamaktır. Bu, test kuralının, kuralın altındaki testin sağlayıcılarına erişmesine izin verir.
Test kuralının uygulama işlevi, onaylamaları yürütür. Herhangi bir hata varsa bu hatalar, fail()
çağrısı yapılarak (analiz zamanı derleme hatasını tetikler) hemen başlatılmaz. Bunun yerine, hataların test yürütme zamanında başarısız olan oluşturulmuş bir komut dosyasında depolanmasıyla yapılır.
Minimal bir oyuncak örneği ve ardından işlemleri kontrol eden bir örnek için aşağıya bakın.
Minimum örnek
//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")
Test, bazel test //mypkg:myrules_test
ile çalıştırılabilir.
İlk load()
ifadelerinin dışında, dosyanın iki ana bölümü vardır:
Testlerin kendisi; 1) test kuralı için bir analiz-zaman uygulama işlevi, 2)
analysistest.make()
aracılığıyla test kuralının bildirilmesi ve 3) az testi (ve bağımlılıklarını) ve test kuralını tanımlamak için bir yükleme süresi işlevi (makro) içerir. Onaylar test durumları arasında değişmiyorsa 1) ve 2) birden fazla test durumu tarafından paylaşılabilir.Her test için yükleme süresi işlevlerini çağıran ve tüm testleri birlikte gruplandıran bir
test_suite
hedefi bildiren test paketi işlevi.
Tutarlılık için önerilen adlandırma kuralını uygulayın: foo
ifadesinin, test adının neyi kontrol ettiğini açıklayan bölümünü temsil etmesine izin verin (yukarıdaki örnekte provider_contents
). Örneğin, bir JUnit test yöntemi testFoo
olarak adlandırılır.
Ardından:
testi ve test edilen hedefi oluşturan makronun adı
_test_foo
(_test_provider_contents
) olmalıdırtest kuralı türü
foo_test
(provider_contents_test
) olarak adlandırılmalıdırbu kural türündeki hedefin etiketi
foo_test
(provider_contents_test
) olmalıdırtest kuralının uygulama işlevi
_foo_test_impl
(_provider_contents_test_impl
) olarak adlandırılmalıdır.test edilen kuralların hedeflerinin etiketleri ve bağımlılıkları
foo_
(provider_contents_
) ön ekini almalıdır
Tüm hedeflerin etiketlerinin, aynı BUILD paketindeki diğer etiketlerle çakışabileceğini unutmayın. Bu nedenle, test için benzersiz bir ad kullanmak faydalı olur.
Hata testi
Belirli girişler veya belirli durumlarda bir kuralın başarısız olduğunu doğrulamak faydalı olabilir. Bunu analiz test çerçevesi kullanılarak yapılabilir:
analysistest.make
ile oluşturulan test kuralı expect_failure
değerini belirtmelidir:
failure_testing_test = analysistest.make(
_failure_testing_test_impl,
expect_failure = True,
)
Test kuralı uygulaması, gerçekleşen hatanın yapısı (özellikle başarısızlık mesajı) hakkında iddialarda bulunmalıdır:
def _failure_testing_test_impl(ctx):
env = analysistest.begin(ctx)
asserts.expect_failure(env, "This rule should never work")
return analysistest.end(env)
Ayrıca test edilen hedefinizin özel olarak "manuel" olarak etiketlendiğinden emin olun.
Bu olmadan, :all
kullanarak paketinizdeki tüm hedefleri derlemek, kasıtlı olarak başarısız olan hedefin oluşturulmasına neden olur ve derleme hatası sergiler. "Manuel" seçeneğinde test edilen hedefiniz, yalnızca açıkça belirtildiğinde veya manuel olmayan bir hedefin (test kuralınız gibi) bağımlılığı olarak oluşturulur:
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.
Kayıtlı işlemleri doğrulama
Örneğin, ctx.actions.run()
kullanarak kuralınızın kaydettiği işlemler hakkında onaylamalar yapan testler yazmak isteyebilirsiniz. Bu, analiz test kuralı uygulama
fonksiyonunuzda yapılabilir. Örnek:
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)
işlevinin, test edilen hedef tarafından kaydedilen işlemleri temsil eden Action
nesnelerinin bir listesini döndürdüğünü unutmayın.
Farklı işaretler altında kural davranışını doğrulama
Gerçek kuralınızın, belirli yapı işaretleriyle belirli bir şekilde davrandığını doğrulamak isteyebilirsiniz. Örneğin, bir kullanıcı şunu belirtirse kuralınız farklı şekilde davranabilir:
bazel build //mypkg:real_target -c opt
ile
bazel build //mypkg:real_target -c dbg
İlk bakışta bu, istenen derleme işaretleriyle test edilen hedefi test ederek yapılabilir:
bazel test //mypkg:myrules_test -c opt
Ancak bu durumda test paketinizin, -c opt
altındaki kural davranışını doğrulayan bir testi ve -c dbg
kapsamındaki kural davranışını doğrulayan başka bir testi aynı anda içermesi imkansız hale gelir. Her iki test de aynı derleme
içinde çalıştırılamaz.
Bu, test kuralını tanımlarken istenen derleme işaretlerini belirterek çözülebilir:
myrule_c_opt_test = analysistest.make(
_myrule_c_opt_test_impl,
config_settings = {
"//command_line_option:compilation_mode": "opt",
},
)
Normalde test edilen bir hedef, mevcut derleme işaretleri göz önünde bulundurularak analiz edilir.
config_settings
değerinin belirtilmesi, belirtilen komut satırı seçeneklerinin değerlerini geçersiz kılar. (Belirtilmemiş tüm seçenekler, gerçek komut satırındaki değerlerini korur).
Belirtilen config_settings
sözlüğünde, komut satırı işaretleri yukarıda gösterildiği gibi //command_line_option:
özel yer tutucu değeriyle öne çıkarılmalıdır.
Yapıları doğrulama
Oluşturulan dosyaların doğru olup olmadığını kontrol etmenin başlıca yolları şunlardır:
Kabuk, Python veya başka bir dilde bir test komut dosyası yazabilir ve uygun
*_test
kural türünde bir hedef oluşturabilirsiniz.Yapmak istediğiniz testin türü için özel bir kural kullanabilirsiniz.
Test hedefi kullanma
Bir yapıyı doğrulamanın en basit yolu, bir komut dosyası yazmak ve BUILD dosyanıza bir *_test
hedefi eklemektir. Kontrol etmek istediğiniz belirli yapılar bu hedefin veri bağımlılıkları olmalıdır. Doğrulama mantığınız birden fazla test için yeniden kullanılabiliyorsa test hedefinin args
özelliği tarafından kontrol edilen komut satırı bağımsız değişkenlerini alan bir komut dosyası olmalıdır. Yukarıdaki myrule
çıkışının "abc"
olduğunu doğrulayan bir örneği aşağıda görebilirsiniz.
//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"],
)
Özel kural kullanma
Daha karmaşık bir alternatif ise kabuk komut dosyasını, yeni bir kuralla örneklenen bir şablon olarak yazmaktır. Bu yöntem daha fazla yönlendirme ve Starlark mantığını içerir ancak BUILD dosyalarının daha temiz olmasını sağlar. Bir yan avantaj olarak, herhangi bir bağımsız değişken ön işlemesi komut dosyası yerine Starlark'ta gerçekleştirilebilir ve komut dosyası, sayısal olanlar (değişmeler için) yerine sembolik yer tutucular (bağımsız değişkenler için) kullandığından biraz daha kendini belgeleme yapar.
//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",
)
Alternatif olarak, şablon genişletme işlemi kullanmak yerine, şablonu .bzl dosyasına dize olarak satır içi yapabilir ve analiz aşamasında str.format
yöntemini veya %
biçimlendirmesini kullanarak genişletmeyi kullanabilirdiniz.
Starlark kamu hizmetlerini test etme
Skylib'in unittest.bzl
çerçevesi, yardımcı program işlevlerini (yani makro veya kural uygulaması olmayan işlevler) test etmek için kullanılabilir. unittest.bzl
adlı uygulamanın analysistest
kitaplığını kullanmak yerine unittest
kullanılabilir. Bu tür test paketlerinde ortak metni azaltmak için unittest.suite()
yardımcı işlevi kullanılabilir.
//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")
Daha fazla örnek için Skylib'in kendi testlerine bakın.