Test

Sorun bildir Kaynağı göster

Bazel'de Starlark kodunu test etmek için birkaç farklı yaklaşım vardır. Bu sayfa, kullanım alanına göre mevcut en iyi uygulamalar ve çerçeveleri toplar.

Kuralları test etme

Skylib, kuralların işlemleri ve sağlayıcıları gibi kuralların analiz zamanı davranışını kontrol etmek için unittest.bzl adlı bir test çerçevesine sahiptir. 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 onaylamaları ayrı bir test çalıştırıcı işleminde değil, derleme içinde gerçekleşir. Test tarafından oluşturulan hedefler, diğer testlerdeki veya derlemedeki hedeflerle çakışmayacak şekilde adlandırılmalıdır. Bazel, test sırasında oluşan bir hatayı test hatası yerine derleme bozulması olarak görür.

  • Test edilen kuralları ve test onaylarını içeren kuralları ayarlamak için yeterli miktarda ortak metin gerekir. Bu standart metin başlangıçta gözünüzü korkutabilir. Makroların, yükleme aşamasında değerlendirildiğini ve hedeflerin oluşturulduğunu unutmayın. Kural uygulama işlevleri ise daha sonraki bir zamana, analiz aşamasında çalışmaya başlamaz.

  • Analysis testlerinin oldukça küçük ve hafif olması amaçlanmıştır. Analiz testi çerçevesinin belirli özellikleri, maksimum sayıda geçişli bağımlılık (şu anda 500) olan hedeflerin doğrulanmasıyla sınırlıdır. Bunun nedeni, bu özellikleri daha büyük testlerle kullanmanın performans üzerindeki etkileridir.

Temel ilke, test etme kuralına bağlı bir test kuralı tanımlamaktır. Bu, test kuralının test altında kural sağlayıcılarına erişmesini sağlar.

Test kuralının uygulama işlevi, onaylamaları gerçekleştirir. Herhangi bir hata varsa bunlar fail() çağrısıyla hemen değil (analiz zamanı derleme hatasını tetikler) çağırarak değil, hataları test yürütme zamanında başarısız olan oluşturulmuş bir komut dosyasında depolayarak yükseltilir.

Minimal oyuncak örneğini, ardından işlemleri kontrol eden bir örneği aşağıda görebilirsiniz.

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 kısmı vardır:

  • Testlerin kendisi şunlardan oluşur: 1) test kuralı için bir analiz zamanı uygulama işlevi, 2) analysistest.make() aracılığıyla test kuralının bildirilmesi ve 3) alt kural testi (ve bağımlılıklarını) ile test kuralını tanımlamak için bir yükleme süresi işlevi (makro). Test durumları arasında iddialar 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 paketleyen bir test_suite hedefi tanımlayan test paketi işlevi.

Tutarlılık için önerilen adlandırma kuralını izleyin: foo ifadesinin, test adının testin neyi kontrol ettiğini açıklayan bölümünü ifade 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ır.

  • test kuralı türü foo_test (provider_contents_test) olarak adlandırılmalıdır.

  • bu kural türündeki hedefin etiketi foo_test (provider_contents_test) olmalıdır.

  • test 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ının önüne foo_ (provider_contents_) eklenmelidir.

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ıdır.

Hata testi

Bir kuralın belirli girişler göz önünde bulundurulduğunda veya belirli bir durumda başarısız olduğunu doğrulamak yararlı olabilir. Bu işlem, analiz testi ç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 (özellikle hata mesajı) yapısı 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. Aksi takdirde, paketinizdeki tüm hedefleri :all kullanarak oluşturmak, kasıtlı olarak başarısız olan hedefin oluşturulmasına neden olur ve bir derleme hatası olur. "Manuel" seçeneğinde, test edilen hedefiniz yalnızca açıkça belirtilirse 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şlemler doğrulanıyor

Örneğin, ctx.actions.run() kullanarak kuralınızın kaydettiği işlemler hakkında iddialarda bulunan testler yazabilirsiniz. Bunu analiz test kuralı uygulama fonksiyonunuzda yapabilirsiniz. Ö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ındaki kural davranışını doğrulama

Belirli derleme işaretlerine göre gerçek kuralınızın belirli bir şekilde davrandığını doğrulamak isteyebilirsiniz. Örneğin, bir kullanıcı şunları belirtirse kuralınız farklı davranabilir:

bazel build //mypkg:real_target -c opt

ile

bazel build //mypkg:real_target -c dbg

İlk bakışta bu, istenen derleme işaretlerini kullanarak test edilen hedefi test ederek yapılabilir:

bazel test //mypkg:myrules_test -c opt

Ancak bu durumda test paketinizin, -c opt kapsamındaki kural davranışını doğrulayan bir test ile -c dbg kapsamındaki kural davranışını doğrulayan başka bir testi aynı anda içermesi mümkün olmaz. İki test de aynı derlemede çalıştırılamaz.

Bu, test kuralını tanımlarken istenen derleme bayraklarının belirtilmesiyle çö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 bayraklarına göre analiz edilir. config_settings belirtilmesi, belirtilen komut satırı seçeneklerinin değerlerini geçersiz kılar. (Belirtilmemiş tüm seçeneklerin değerleri gerçek komut satırındaki değerleri korur).

Belirtilen config_settings sözlüğünde, komut satırı işaretlerinin önüne yukarıda gösterildiği gibi //command_line_option: özel yer tutucu değeri eklenmelidir.

Yapılar doğrulanıyor

Oluşturulan dosyaların doğru olup olmadığını kontrol etmenin başlıca yolları:

  • Kabuk, Python veya başka bir dilde bir test komut dosyası yazıp uygun *_test kural türünde bir hedef oluşturabilirsiniz.

  • Gerçekleştirmek istediğiniz test türü için özel bir kural kullanabilirsiniz.

Test hedefi kullanma

Bir yapıyı doğrulamanın en basit yolu, komut dosyası yazıp 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 çıktısının "abc" olduğunu doğrulayan bir örneği burada bulabilirsiniz.

//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 kullanarak

Kabuk komut dosyasını, yeni bir kural tarafından somutlaşan bir şablon olarak yazmak daha karmaşık bir alternatiftir. Bu, daha fazla dolaylı ve Starlark mantığını içerir, ancak daha temiz DERLEME dosyaları sağlar. Bu yöntemin diğer bir avantajı da, komut dosyası yerine Starlark'ta bağımsız değişken ön işlemesi yapılmasıdır. Komut dosyası, sayısal yer tutucular (bağımsız değişkenler için) yerine sembolik yer tutucular (değişiklikler için) kullandığından biraz daha kendi kendini belgelemektedir.

//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 bir dize olarak satır içi olarak ekleyebilir ve analiz aşamasında str.format yöntemini veya % biçimlendirmesini kullanarak genişletebilirsiniz.

Starlark yardımcı programlarını test etme

Skylib'in unittest.bzl çerçevesi, yardımcı program işlevlerini (yani makro veya kural uygulaması olmayan işlevleri) test etmek için kullanılabilir. unittest.bzl adlı çocuğun analysistest kitaplığının yerine unittest kullanılabilir. Bu tür test paketlerinde, ortak metni azaltmak için unittest.suite() kolaylık 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.