การทดสอบโค้ด Starlark ใน Bazel ทำได้หลายวิธี หน้านี้รวบรวมแนวทางปฏิบัติแนะนำและเฟรมเวิร์กปัจจุบันตาม Use Case
กฎการทดสอบ
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_
) เป็นคำนำหน้า
โปรดทราบว่าป้ายกำกับของเป้าหมายทั้งหมดอาจขัดแย้งกับป้ายกำกับอื่นๆ ในแพ็กเกจ "สร้าง" เดียวกัน จึงควรใช้ชื่อที่ไม่ซ้ำกันสำหรับการทดสอบ
การทดสอบข้อผิดพลาด
การยืนยันว่ากฎไม่ผ่านการตรวจสอบเมื่อป้อนข้อมูลบางอย่างหรืออยู่ในสถานะบางอย่างอาจมีประโยชน์ ซึ่งทำได้โดยใช้เฟรมเวิร์กทดสอบการวิเคราะห์
กฎทดสอบที่สร้างด้วย 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
จะส่งผลให้มีการสร้างขึ้นของเป้าหมายที่จงใจให้ใช้งานไม่ได้และจะแสดงการสร้างที่ไม่สําเร็จ เมื่อใช้ค่า "manual" ระบบจะสร้างเป้าหมายที่ทดสอบก็ต่อเมื่อระบุไว้อย่างชัดเจน หรือเป็นข้อกําหนดของเป้าหมายที่ไม่ใช่ "manual" (เช่น กฎการทดสอบ)
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
เมื่อมองแวบแรก การดำเนินการนี้อาจทำได้โดยการทดสอบเป้าหมายที่ทดสอบโดยใช้ Flag การสร้างที่ต้องการ ดังนี้
bazel test //mypkg:myrules_test -c opt
แต่ชุดทดสอบของคุณจะมีทั้งการทดสอบที่ยืนยันลักษณะการทํางานของกฎภายใต้ -c opt
และการทดสอบอื่นที่ยืนยันลักษณะการทํางานของกฎภายใต้ -c dbg
ไม่ได้ การทดสอบทั้ง 2 รายการจะทํางานในบิลด์เดียวกันไม่ได้
ปัญหานี้แก้ไขได้โดยระบุ Flag การสร้างที่ต้องการเมื่อกำหนดกฎการทดสอบ ดังนี้
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