การทดสอบ

รายงานปัญหา ดูแหล่งที่มา รุ่น Nightly · 7.4 7.3 · 7.2 · 7.1 · 7.0 · 6.5

การทดสอบโค้ด Starlark ใน Bazel ทำได้หลายวิธี หน้านี้จะรวบรวมแนวทางปฏิบัติแนะนำและเฟรมเวิร์กในปัจจุบันตามกรณีการใช้งาน

กฎการทดสอบ

Skylib มีเฟรมเวิร์กการทดสอบที่เรียกว่า unittest.bzl สำหรับตรวจสอบลักษณะการทํางานของกฎในเวลาวิเคราะห์ เช่น การดําเนินการและผู้ให้บริการ การทดสอบดังกล่าวเรียกว่า "การทดสอบการวิเคราะห์" และปัจจุบันเป็นตัวเลือกที่ดีที่สุดสำหรับการทดสอบการทํางานภายในของกฎ

ข้อควรระวังบางประการมีดังนี้

  • การยืนยันการทดสอบจะเกิดขึ้นภายในบิลด์ ไม่ใช่กระบวนการของตัวดำเนินการทดสอบที่แยกต่างหาก เป้าหมายที่สร้างขึ้นจากการทดสอบต้องตั้งชื่อให้ไม่ทับซ้อนกับเป้าหมายจากการทดสอบอื่นๆ หรือจากบิลด์ Bazel มองว่าข้อผิดพลาดที่เกิดขึ้นระหว่างการทดสอบเป็นการหยุดทำงานของเวอร์ชัน ไม่ใช่ความล้มเหลวในการทดสอบ

  • ต้องใช้ข้อความที่ซ้ำกันค่อนข้างมากในการตั้งค่ากฎที่อยู่ระหว่างการทดสอบและกฎที่มีข้อความยืนยันการทดสอบ ข้อมูลเทมเพลตนี้อาจดูน่ากลัวในตอนแรก โปรดทราบว่ามาโครจะได้รับการประเมินและเป้าหมายที่สร้างขึ้นในระหว่างขั้นตอนการโหลด ขณะที่ฟังก์ชันการใช้งานกฎจะไม่ทำงานจนกระทั่งภายหลังในระหว่างช่วงการวิเคราะห์

  • การทดสอบการวิเคราะห์มีวัตถุประสงค์ให้มีจำนวนค่อนข้างเล็กและใช้ทรัพยากรน้อย ฟีเจอร์บางอย่างของเฟรมเวิร์กการทดสอบการวิเคราะห์จะจํากัดไว้สําหรับการยืนยันเป้าหมายที่มีจำนวนการพึ่งพาแบบเปลี่ยนผ่านสูงสุด (ปัจจุบันคือ 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() เริ่มต้นแล้ว ไฟล์ยังมี 2 ส่วนหลักๆ ดังนี้

  • การทดสอบแต่ละรายการประกอบด้วย 1) ฟังก์ชันการใช้งานขณะวิเคราะห์สําหรับกฎการทดสอบ 2) การประกาศกฎการทดสอบผ่าน analysistest.make() และ 3) ฟังก์ชันเวลาในการโหลด (มาโคร) สําหรับการประกาศกฎที่อยู่ระหว่างการทดสอบ (และรายการที่เกี่ยวข้อง) และกฎการทดสอบ หากการยืนยันไม่มีการเปลี่ยนแปลงระหว่างชุดทดสอบ 1) และ 2) อาจใช้ร่วมกันในหลายชุดทดสอบ

  • ฟังก์ชันชุดทดสอบ ซึ่งเรียกใช้ฟังก์ชันเวลาในการโหลดสําหรับการทดสอบแต่ละรายการ และประกาศเป้าหมาย 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 จะส่งผลให้มีการสร้างขึ้นของเป้าหมายที่จงใจให้ใช้งานไม่ได้และจะแสดงการสร้างที่ไม่สําเร็จ เมื่อใช้ "ด้วยตนเอง" เป้าหมายที่อยู่ระหว่างการทดสอบจะสร้างก็ต่อเมื่อมีการระบุอย่างชัดเจน หรือเป็นทรัพยากร Dependency ของเป้าหมายที่ไม่ใช่ด้วยตนเอง (เช่น กฎการทดสอบ) ดังนี้

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 ซึ่งแสดงการดําเนินการที่เป้าหมายทดสอบบันทึกไว้

การยืนยันลักษณะการทํางานของกฎภายใต้ Flag ต่างๆ

คุณอาจต้องตรวจสอบว่ากฎจริงทำงานในลักษณะหนึ่งๆ เมื่อใช้ Flag การสร้างบางอย่าง เช่น กฎอาจทํางานแตกต่างกันหากผู้ใช้ระบุข้อมูลต่อไปนี้

bazel build //mypkg:real_target -c opt

ปะทะกับ

bazel build //mypkg:real_target -c dbg

เมื่อมองแวบแรก การดำเนินการนี้อาจทำได้โดยการทดสอบเป้าหมายที่ทดสอบโดยใช้ Flag การสร้างที่ต้องการ ดังนี้

bazel test //mypkg:myrules_test -c opt

แต่ชุดทดสอบของคุณจะมีทั้งการทดสอบที่ยืนยันลักษณะการทํางานของกฎภายใต้ -c opt และการทดสอบอื่นที่ยืนยันลักษณะการทํางานของกฎภายใต้ -c dbg ไม่ได้ การทดสอบทั้ง 2 รายการจะทํางานในบิลด์เดียวกันไม่ได้

ปัญหานี้แก้ได้โดยการระบุแฟล็กบิลด์ที่ต้องการเมื่อกำหนดกฎทดสอบ ดังนี้

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

โดยทั่วไป ระบบจะวิเคราะห์เป้าหมายที่ทดสอบตาม Flag บิลด์ปัจจุบัน การระบุ 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 มากขึ้น แต่ทำให้ไฟล์ BUILD สะอาดขึ้น ประโยชน์เพิ่มเติมคือ คุณสามารถทำการจัดเตรียมข้อมูลล่วงหน้าของอาร์กิวเมนต์ใน 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 ของ Skylib สามารถใช้เพื่อทดสอบฟังก์ชันยูทิลิตี (นั่นคือ ฟังก์ชันที่ไม่ใช่มาโครหรือการใช้งานกฎ) ระบบอาจใช้ unittest แทนการใช้ไลบรารี analysistest ของ unittest.bzl สําหรับชุดทดสอบดังกล่าว คุณสามารถใช้ฟังก์ชันเพื่อความสะดวก 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