Test

Sorun bildir Kaynağı göster Nightly · 8.0 · 7.4 · 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Bazel'de Starlark kodunu test etmenin birkaç farklı yolu vardır. Bu sayfada, kullanım alanına göre geçerli en iyi uygulamalar ve çerçeveler toplanmıştır.

Test kuralları

Skylib'de, kuralların analiz sırasındaki davranışını (ör. işlemleri 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şleyişini test etmek için en iyi seçenektir.

Bazı noktalar:

  • Test iddiaları, ayrı bir test çalıştırıcı sürecinde 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. Test sırasında oluşan hatalar Bazel tarafından test hatası yerine derleme hatası olarak görülür.

  • Test edilen kuralları ve test beyanları içeren kuralları ayarlamak için oldukça fazla standart metin gerekir. Bu şablon ilk başta göz korkutucu görünebilir. Makroların yükleme aşamasında değerlendirildiğini ve hedeflerin oluşturulduğunu, kural uygulama işlevlerinin ise daha sonra analiz aşamasında çalıştırıldığını hatırlamanız faydalı olacaktır.

  • Analiz testlerinin oldukça küçük ve hafif olması amaçlanır. Analiz testi çerçevesinin belirli özellikleri, maksimum sayıda geçişli bağımlılığa (şu anda 500) sahip hedefleri doğrulamakla sınırlıdır. Bunun nedeni, bu özelliklerin daha büyük testlerle kullanılmasının performans üzerindeki etkileridir.

Temel prensip, test edilen kurala bağlı bir test kuralı tanımlamaktır. Bu, test kuralının test edilen kuralın sağlayıcılarına erişmesine olanak tanır.

Test kuralının uygulama işlevi, iddiaları gerçekleştirir. Herhangi bir hata varsa bunlar fail() çağrılarak hemen oluşturulmaz (bu, analiz sırasında derleme hatası tetikler). Bunun yerine, hatalar test yürütme sırasında başarısız olan oluşturulmuş bir komut dosyasında saklanır.

Minimal bir oyuncak örneği ve ardından işlemleri kontrol eden bir örnek için aşağıya bakın.

Minimal ö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.

Dosyanın ilk load() beyanları dışında iki ana bölümü vardır:

  • Testlerin kendisi. Her biri 1) test kuralı için analiz zamanındaki bir uygulama işlevi, 2) analysistest.make() aracılığıyla test kuralının beyanı ve 3) test edilen kuralı (ve bağımlılıkları) ve test kuralını beyan etmek için yükleme zamanındaki bir işlev (makro) içerir. İddialar test durumları arasında değişmezse 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 bir araya getiren bir test_suite hedefi açıklayan test paketi işlevi.

Tutarlılık için önerilen adlandırma kuralını uygulayın: foo, test adının testin neyi kontrol ettiğini açıklayan kısmını (yukarıdaki örnekte provider_contents) temsil etsin. Örneğin, bir JUnit test yöntemi testFoo olarak adlandırılır.

Ardından:

  • Testi ve test edilen hedefi oluşturan makro _test_foo (_test_provider_contents) olarak adlandırılmalıdır.

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

  • Bu kural türünün hedefinin etiketi foo_test olmalıdır (provider_contents_test)

  • 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 bunların bağımlılıkları foo_ (provider_contents_) ile öneklendirilmelidir.

Tüm hedeflerin etiketlerinin aynı BUILD paketindeki diğer etiketlerle çakışabileceğini unutmayın. Bu nedenle, test için benzersiz bir ad kullanmanız faydalı olacaktır.

Hata testi

Belirli girişler veya belirli bir durumda bir kuralın 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'ı belirtmelidir:

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

Test kuralı uygulaması, gerçekleşen hatanın niteliği (özellikle hata 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 "manuel" olarak etiketlendiğinden emin olun. Bu olmadan, paketinizdeki tüm hedefleri :all kullanarak oluşturmak, kasıtlı olarak başarısız olan hedefin oluşturulmasına ve derleme hatası gösterilmesine neden olur. "Manuel" seçeneğiyle, 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şlemleri doğrulama

Kuralınızın kaydettiği işlemler hakkında iddialarda bulunan testler (ör. ctx.actions.run() kullanarak) yazabilirsiniz. Bu işlem, analiz testi kural uygulama işlevinizde 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ındaki kural davranışını doğrulama

Gerçek kuralınızın belirli derleme işaretleri verildiğinde belirli bir şekilde davrandığını doğrulamak isteyebilirsiniz. Örneğin, bir kullanıcı aşağıdakileri belirtirse kuralınız farklı şekilde davranabilir:

bazel build //mypkg:real_target -c opt

karşı

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 aynı anda -c opt altındaki kural davranışını doğrulayan bir test ve -c dbg altındaki kural davranışını doğrulayan başka bir test içermesi imkansız hale gelir. Her iki test de aynı derlemede çalıştırılamaz.

Bu sorun, 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",
    },
)

Test altındaki bir hedef normalde mevcut derleme işaretleri dikkate alınarak analiz edilir. config_settings belirtildiğinde, belirtilen komut satırı seçeneklerinin değerleri geçersiz kılınır. (Belirtilmeyen seçenekler, gerçek komut satırındaki değerlerini korur).

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

Yapıları doğrulama

Oluşturduğunuz 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ünü içeren bir hedef oluşturabilirsiniz.

  • Yapmak istediğiniz test türü için özel bir kural kullanabilirsiniz.

Test hedefi kullanma

Bir yapıyı doğrulamanın en kolay yolu, bir komut dosyası yazıp BUILD dosyanıza *_test hedefi eklemektir. Kontrol etmek istediğiniz belirli yapı taşlarının bu hedefin veri bağımlılıkları olması gerekir. Doğrulama mantığınız birden fazla test için yeniden kullanılabilirse 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 işlevinin çıkışının "abc" olduğunu doğrulayan bir örnek verilmiştir.

//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, kabuk komut dosyasını yeni bir kural tarafından örneklendirilen bir şablon olarak yazmaktır. Bu yöntem daha fazla dolaylı referans ve Starlark mantığı içerir ancak daha temiz BUILD dosyalarına yol açar. Bu yaklaşımın yan faydalarından biri, tüm ön işleme işlemlerinin komut dosyası yerine Starlark'ta yapılabilmesidir. Ayrıca, komut dosyası sayısal yerine sembolik yer tutucular (yerine koyma işlemleri için) kullandığından kendi kendine açıklama özelliği biraz daha fazladır.

//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 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ı işlevleri (yani makro veya kural uygulaması olmayan işlevler) test etmek için kullanılabilir. unittest.bzl'nin analysistest kitaplığı yerine unittest kullanılabilir. Bu tür test paketlerinde, ortak metinleri 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.