الاختبار

هناك طرق مختلفة لاختبار رمز Starlark في Bazel. تجمع هذه الصفحة أفضل الممارسات وأُطر العمل الحالية حسب حالة الاستخدام.

قواعد الاختبار

لدى Skylib إطار عمل تجريبي يُسمى unittest.bzl لفحص سلوك قواعد تحليل الوقت، مثل الإجراءات ومقدّمي الخدمات. وتُعرف هذه الاختبارات باسم "اختبارات التحليل"، وهي أفضل خيار حاليًا لاختبار العمل الداخلي للقواعد.

بعض التنبيهات:

  • تحدث تأكيدات الاختبارات داخل الإصدار، وليست عملية تشغيل تجريبي منفصلة. يجب تسمية الأهداف التي ينشئها الاختبار بحيث لا تتعارض مع الأهداف من الاختبارات الأخرى أو من الإصدار. وينظر "بازيل" إلى الخطأ الذي يحدث أثناء الاختبار باعتباره كسرًا في الإصدار وليس كفشل في الاختبار.

  • يجب تقديم مبلغ معياري من النص المعياري لإعداد القواعد قيد الاختبار والقواعد التي تحتوي على تأكيدات الاختبار. قد تبدو هذه النص النموذجي شاقة في البداية. ويساعد ذلك في تنبيه أنه يتم تقييم وحدات الماكرو والأهداف التي يتم إنشاؤها أثناء مرحلة التحميل، في حين لا يتم تشغيل وظائف تنفيذ القواعد إلا في وقت لاحق، أثناء مرحلة التحليل.

  • تهدف اختبارات التحليل إلى أن تكون صغيرة نوعًا ما وخفيفة الوزن. تنحصر ميزات معيّنة لإطار عمل اختبار التحليل في التحقّق من الأهداف التي بلغت الحدّ الأقصى لعدد العناصر المسموح بها التي تتجاوز الحد المسموح به (500 حاليًا). ويرجع هذا إلى الآثار المترتبة على استخدام هذه الميزات في الاختبارات الأكبر.

المبدأ الأساسي هو تحديد قاعدة اختبار تعتمد على القاعدة تحت الاختبار. ويحصل هذا الإجراء على قاعدة اختبار الوصول لموفّري القاعدة تحت الاختبار.

تنفِّذ وظيفة تنفيذ قاعدة الاختبار تأكيدات. إذا لم تكن هناك أي إخفاقات، لا يتم عرض هذه الأخطاء على الفور من خلال استدعاء fail() (الذي سيؤدي إلى حدوث خطأ في إصدار وقت التحليل)، ولكن بدلاً من ذلك عن طريق تخزين الأخطاء في نص برمجي تم إنشاؤه تتعذّر في وقت التنفيذ.

يمكنك الاطّلاع أدناه على مثال بسيط للعبة، يليه مثال للتحقق من الإجراءات.

مثال بسيط

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

يمكن إجراء الاختبار باستخدام bazel test //mypkg:myrules_test.

إلى جانب كشوفات الحساب الأولية من load()، هناك جزءان رئيسيان في الملف:

  • الاختبارات نفسها، ويتألف كل منها من: 1) وظيفة تنفيذ في وقت التحليل لقاعدة الاختبار، و2) بيان قاعدة الاختبار عبر analysistest.make()، و3) دالة وقت التحميل (ماكرو) للتصريح عن القاعدة الخاضعة للاختبار (وتبعياتها) واختبار القاعدة. في حال عدم تغيّر التأكيدات بين حالات الاختبار 1 و1) يمكن مشاركتها من خلال عدة حالات اختبار.

  • وظيفة حزمة الاختبار، والتي تستدعي وظائف وقت التحميل لكل اختبار، وتعلن عن هدف test_suite يجمع كل الاختبارات معًا.

لتحقيق الاتساق، يُرجى اتباع اصطلاح التسمية الموصى به: دع foo يرمز إلى جزء اسم الاختبار الذي يصف ما يتحقق منه الاختبار (provider_contents في المثال أعلاه). على سبيل المثال، ستتم تسمية طريقة اختبار JUnit testFoo.

بعد ذلك:

  • يجب تسمية وحدة الماكرو التي تُنشئ الاختبار والهدف ضمن الاختبار باسم _test_foo (_test_provider_contents).

  • يجب تسمية نوع قاعدة الاختبار foo_test (provider_contents_test).

  • يجب أن يكون تصنيف الهدف من نوع القاعدة هذا foo_test (provider_contents_test).

  • يجب تسمية دالة التنفيذ لقاعدة الاختبار _foo_test_impl (_provider_contents_test_impl)

  • يجب أن تسبق تصنيفات أهداف الأهداف قيد الاختبار وتبعياتها foo_ (provider_contents_)

يُرجى العِلم بأن تصنيفات جميع الأهداف يمكن أن تتعارض مع التصنيفات الأخرى في حزمة BUILD نفسها، لذا من المفيد استخدام اسم فريد للاختبار.

تعذّر الاختبار

قد يكون من المفيد التحقّق من عدم توفّر قاعدة بسبب إدخال معيّن أو في حالة معيّنة. ويمكن إجراء ذلك باستخدام إطار عمل اختبار التحليل:

يجب أن تحدد القاعدة التجريبية التي تم إنشاؤها باستخدام analysistest.make الخاصية expect_failure:

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

من المفترض أن يؤدي تأكيد القاعدة إلى عدم نجاح طبيعة التعذُّر التي حدثت (لا سيما رسالة الخطأ):

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

وتأكّد أيضًا من أن هدفك قيد الاختبار مخصّص يدويًا. بدون ذلك، سيؤدي إنشاء جميع الأهداف في الحزمة باستخدام :all إلى إنشاء استهداف يفشل عمدًا وسيعرض خطأ في الإصدار. باستخدام "اليدوي"، لن يتم إنشاء هدفك تحت الاختبار إلا إذا تم تحديده صراحةً، أو اعتمادًا على هدف غير يدوي (مثل قاعدة الاختبار):

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.

جارٍ التحقّق من الإجراءات المسجّلة

ننصحك بكتابة الاختبارات التي تؤكد الإجراءات التي تسجّلها القاعدة، مثلاً، باستخدام ctx.actions.run(). ويمكن إجراء ذلك في وظيفة تنفيذ قاعدة اختبار التحليل. مثال:

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) تعرض قائمة بكائنات Action تمثل الإجراءات التي تم تسجيلها بواسطة الهدف قيد الاختبار.

التحقّق من سلوك القاعدة ضمن علامات مختلفة

قد تحتاج إلى التحقق من أن قاعدتك الفعلية تتصرف بطريقة معينة في ضوء علامات معينة للإصدار. على سبيل المثال، قد يختلف سلوك القاعدة إذا حدّد المستخدم ما يلي:

bazel build //mypkg:real_target -c opt

مقابل

bazel build //mypkg:real_target -c dbg

للوهلة الأولى، يمكن إجراء ذلك من خلال اختبار الهدف قيد الاختبار باستخدام علامات الإصدار المطلوبة:

bazel test //mypkg:myrules_test -c opt

وبعد ذلك، يصبح من المستحيل أن تحتوي حزمة الاختبار على اختبار في الوقت نفسه يتم فيه التحقق من سلوك القاعدة بموجب -c opt واختبار آخر يثبت سلوك القاعدة ضمن -c dbg. ولن يكون من الممكن إجراء الاختبارين في الإصدار نفسه.

يمكن حل هذه المشكلة من خلال تحديد علامات الإصدار المطلوبة عند تحديد قاعدة الاختبار:

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

وعادةً ما يتم تحليل هدف قيد الاختبار بالنظر إلى علامات الإصدار الحالية. يؤدي تحديد config_settings إلى إلغاء قيم خيارات سطر الأوامر المحدّدة. (ستحتفظ أي خيارات غير محدّدة بقيمها من سطر الأوامر الفعلي).

في قاموس config_settings المحدد، يجب أن تسبق علامات سطر الأوامر قيمة عنصر نائب خاصة //command_line_option:، كما هو موضح أعلاه.

التحقّق من العناصر

الطرق الرئيسية للتحقُّق من صحة الملفات التي تم إنشاؤها هي:

  • يمكنك كتابة نص برمجي تجريبي بلغة Shell أو Python أو لغة أخرى، وإنشاء هدف لنوع القاعدة *_test المناسب.

  • يمكنك استخدام قاعدة مخصّصة لنوع الاختبار الذي تريد إجراؤه.

استخدام هدف تجريبي

أسهل طريقة للتحقّق من العنصر هي كتابة نص برمجي وإضافة هدف *_test إلى ملف BUILD. يجب أن تكون العناصر المعيّنة التي تريد التحقق منها تبعية بيانات لهذا الهدف. إذا كان منطق التحقق من الصحة قابلاً لإعادة الاستخدام في اختبارات متعددة، يجب أن يكون نصًا برمجيًا يحتوي على وسيطات سطر الأوامر، والتي يتم التحكم فيها من خلال سمة args المستهدفة للاختبار. إليك مثال على أنّ ناتج myrule أعلاه هو "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"],
)

استخدام قاعدة مخصصة

هناك بديل أكثر تعقيدًا وهو كتابة نص البرنامج البرمجي كنموذج يتم إنشاؤه استنادًا إلى قاعدة جديدة. ويتضمّن ذلك المزيد من التوجيهات غير المنطقية والمنطقية، ولكن يؤدي إلى إنشاء ملفات أكثر وضوحًا. كميزة مؤقتة، يمكن إجراء أي معالجة مسبقة للوسيطة في Starlark بدلاً من النص البرمجي، ويتم إجراء التوثيق الذاتي إلى حد ما لأنه يستخدم عناصر نائبة رمزية (للاستبدال) بدلاً من البدائل الرقمية. (بالنسبة إلى الوسيطات).

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

بدلاً من استخدام إجراء توسيع النموذج، يمكنك تضمين النموذج في ملف .bzl كسلسلة وتوسيعه أثناء مرحلة التحليل باستخدام طريقة str.format أو %- .

اختبار الأدوات المساعدة من Starlark

سكايليب unittest.bzl يمكن استخدام الإطار لاختبار وظائف الأدوات المساعدة (أي الوظائف التي لا تكون وحدات ماكرو أو عمليات تنفيذ قواعد). بدلاً من استخدام مكتبة unittest.bzl analysistest، قد يتم استخدام unittest. وبالنسبة إلى أجنحة الاختبار هذه، يمكن استخدام الدالة الصغيرة unittest.suite() لتقليل النص النموذجي.

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

للاطّلاع على مزيد من الأمثلة، يمكنك مراجعة اختبارات Skylib الخاصة.