Kiểm thử

Báo cáo sự cố Xem nguồn

Có một số phương pháp để kiểm thử mã Starlark ở Bazel. Trang này tập hợp các khung và phương pháp hay nhất hiện nay theo trường hợp sử dụng.

Quy tắc kiểm thử

Skylib có một khung kiểm thử tên là unittest.bzl để kiểm tra hành vi tại thời điểm phân tích của các quy tắc, chẳng hạn như hành động và trình cung cấp của các quy tắc. Những quy trình kiểm thử như vậy được gọi là "kiểm thử phân tích" và hiện là tuỳ chọn tốt nhất để kiểm thử hoạt động bên trong của các quy tắc.

Một số lưu ý:

  • Xác nhận kiểm thử xuất hiện trong bản dựng, không phải trong một quy trình chạy kiểm thử riêng biệt. Các mục tiêu do quy trình kiểm thử tạo ra phải được đặt tên sao cho các mục tiêu đó không xung đột với các mục tiêu trong các lượt kiểm thử khác hoặc với bản dựng. Bazel xem một lỗi xảy ra trong quá trình kiểm thử là lỗi bản dựng chứ không phải là lỗi kiểm thử.

  • Phương thức này đòi hỏi một lượng mã nguyên mẫu hợp lý để thiết lập các quy tắc đang kiểm thử và các quy tắc chứa câu nhận định kiểm thử. Thoạt đầu, bản mẫu này có vẻ sẽ rất khó khăn. Bạn cần lưu ý rằng macro được đánh giá và các mục tiêu được tạo trong giai đoạn tải, trong khi các hàm triển khai quy tắc chỉ chạy sau đó trong giai đoạn phân tích.

  • Các bài kiểm thử trong công cụ Phân tích thường khá nhỏ và gọn nhẹ. Một số tính năng nhất định của khung kiểm thử phân tích bị hạn chế nhằm xác minh các mục tiêu có số lượng phần phụ thuộc bắc cầu tối đa (hiện là 500). Điều này là do hệ quả về hiệu suất của việc sử dụng các tính năng này với các bài kiểm thử lớn hơn.

Nguyên tắc cơ bản là xác định quy tắc kiểm thử phụ thuộc vào quy tắc đang kiểm thử. Thao tác này cho phép quy tắc kiểm thử này truy cập vào các trình cung cấp của quy tắc đang kiểm thử.

Chức năng triển khai của quy tắc kiểm thử thực hiện các câu nhận định. Nếu có bất kỳ lỗi nào, các lỗi này sẽ không được nêu ngay lập tức bằng cách gọi fail() (sẽ kích hoạt lỗi bản dựng tại thời điểm phân tích), mà sẽ lưu trữ lỗi trong tập lệnh đã tạo nhưng không thành công tại thời điểm thực thi kiểm thử.

Hãy xem ví dụ tối thiểu về đồ chơi ở bên dưới, tiếp theo là ví dụ kiểm tra các hành động.

Ví dụ tối thiểu

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

Bạn có thể chạy kiểm thử bằng bazel test //mypkg:myrules_test.

Ngoài các câu lệnh load() ban đầu, tệp này có 2 phần chính:

  • Bản thân quá trình kiểm thử, mỗi hàm bao gồm 1) hàm triển khai tại thời điểm phân tích cho quy tắc kiểm thử, 2) khai báo quy tắc kiểm thử thông qua analysistest.make() và 3) hàm thời gian tải (macro) để khai báo quy tắc kiểm thử (và các phần phụ thuộc của quy tắc này) và quy tắc kiểm thử. Nếu các câu nhận định không thay đổi giữa các trường hợp kiểm thử, thì 1) và 2) có thể được nhiều trường hợp kiểm thử chia sẻ.

  • Hàm bộ kiểm thử, gọi hàm thời gian tải cho mỗi bài kiểm thử và khai báo mục tiêu test_suite gói tất cả bài kiểm thử lại với nhau.

Để đảm bảo tính nhất quán, hãy tuân theo quy ước đặt tên đề xuất: Đặt foo là đại diện cho phần tên kiểm thử mô tả nội dung kiểm thử đang kiểm tra (provider_contents trong ví dụ trên). Ví dụ: phương thức thử nghiệm JUnit sẽ có tên là testFoo.

Sau đó:

  • macro tạo ra hoạt động kiểm thử và mục tiêu đang được kiểm thử phải được đặt tên là _test_foo (_test_provider_contents)

  • loại quy tắc thử nghiệm của báo cáo phải được đặt tên là foo_test (provider_contents_test)

  • nhãn của mục tiêu của loại quy tắc này phải là foo_test (provider_contents_test)

  • hàm triển khai quy tắc kiểm thử phải được đặt tên là _foo_test_impl (_provider_contents_test_impl)

  • nhãn của mục tiêu của các quy tắc đang được kiểm thử và phần phụ thuộc của các quy tắc đó phải có tiền tố foo_ (provider_contents_)

Hãy lưu ý rằng nhãn của tất cả mục tiêu có thể xung đột với các nhãn khác trong cùng một gói XÂY DỰNG, vì vậy bạn nên sử dụng tên duy nhất cho hoạt động kiểm thử này.

Kiểm thử không thành công

Việc xác minh rằng một quy tắc không thành công nếu có một số dữ liệu đầu vào hoặc ở trạng thái nhất định. Bạn có thể làm việc này bằng cách sử dụng khung kiểm tra phân tích:

Quy tắc kiểm thử được tạo bằng analysistest.make phải chỉ định expect_failure:

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

Quá trình triển khai quy tắc kiểm thử cần đưa ra xác nhận về bản chất của lỗi xảy ra (cụ thể là thông báo lỗi):

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

Ngoài ra, hãy đảm bảo rằng mục tiêu đang thử nghiệm được gắn thẻ riêng là "thủ công". Nếu không thực hiện điều này, việc tạo tất cả các mục tiêu trong gói bằng :all sẽ dẫn đến việc tạo bản dựng mục tiêu không đạt một cách có chủ ý và sẽ xuất hiện lỗi bản dựng. Khi đặt mục tiêu "thủ công", mục tiêu đang được kiểm thử của bạn sẽ chỉ được tạo nếu được chỉ định rõ ràng hoặc dưới dạng phần phụ thuộc của mục tiêu không thủ công (chẳng hạn như quy tắc kiểm thử của bạn):

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.

Xác minh các hành động đã đăng ký

Bạn nên viết các mã kiểm thử đưa ra xác nhận về các hành động mà quy tắc của bạn đăng ký, chẳng hạn như bằng cách sử dụng ctx.actions.run(). Bạn có thể thực hiện việc này trong hàm triển khai quy tắc kiểm thử phân tích. Ví dụ:

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)

Lưu ý rằng analysistest.target_actions(env) trả về danh sách các đối tượng Action đại diện cho các hành động do mục tiêu đăng ký đang được kiểm thử.

Xác minh hành vi của quy tắc dưới các cờ khác nhau

Bạn nên xác minh rằng quy tắc thực của mình hoạt động theo một cách nhất định dựa trên một số cờ bản dựng nhất định. Ví dụ: quy tắc của bạn có thể hoạt động theo cách khác nếu người dùng chỉ định:

bazel build //mypkg:real_target -c opt

so với

bazel build //mypkg:real_target -c dbg

Thoạt nhìn, bạn có thể thực hiện việc này bằng cách kiểm thử mục tiêu đang được kiểm thử bằng cách sử dụng cờ bản dựng mong muốn:

bazel test //mypkg:myrules_test -c opt

Tuy nhiên, bộ kiểm thử của bạn sẽ không thể đồng thời chứa một quy trình kiểm thử xác minh hành vi của quy tắc trong -c opt và một quy trình kiểm thử khác xác minh hành vi của quy tắc trong -c dbg. Cả hai bài kiểm thử sẽ không thể chạy trong cùng một bản dựng!

Điều này có thể được giải quyết bằng cách chỉ định cờ bản dựng mong muốn khi xác định quy tắc kiểm thử:

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

Thông thường, một mục tiêu đang kiểm thử sẽ được phân tích dựa trên cờ bản dựng hiện tại. Thao tác chỉ định config_settings sẽ ghi đè các giá trị của tuỳ chọn dòng lệnh được chỉ định. (Mọi tuỳ chọn chưa được chỉ định sẽ giữ lại các giá trị từ dòng lệnh thực tế).

Trong từ điển config_settings đã chỉ định, cờ dòng lệnh phải có tiền tố là giá trị phần giữ chỗ đặc biệt //command_line_option:, như minh hoạ ở trên.

Xác thực cấu phần phần mềm

Sau đây là những cách chính để kiểm tra nhằm đảm bảo rằng các tệp được tạo của bạn là chính xác:

  • Bạn có thể viết tập lệnh kiểm thử bằng shell, Python hoặc ngôn ngữ khác và tạo mục tiêu của loại quy tắc *_test thích hợp.

  • Bạn có thể sử dụng một quy tắc chuyên biệt cho loại kiểm thử mà bạn muốn thực hiện.

Sử dụng mục tiêu kiểm thử

Cách đơn giản nhất để xác thực cấu phần phần mềm là viết tập lệnh và thêm mục tiêu *_test vào tệp BUILD. Các cấu phần phần mềm cụ thể mà bạn muốn kiểm tra phải là phần phụ thuộc dữ liệu của mục tiêu này. Nếu logic xác thực của bạn có thể sử dụng lại cho nhiều lần kiểm thử, thì đó phải là một tập lệnh nhận các đối số dòng lệnh do thuộc tính args của mục tiêu kiểm thử kiểm soát. Dưới đây là ví dụ xác thực rằng đầu ra của myrule ở trên là "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"],
)

Sử dụng quy tắc tuỳ chỉnh

Một cách khác phức tạp hơn là viết tập lệnh shell làm mẫu được tạo thực thể bởi một quy tắc mới. Việc này bao gồm nhiều yếu tố gián tiếp và logic Starlark hơn, nhưng dẫn đến các tệp BUILD gọn gàng hơn. Lợi ích phụ là mọi việc xử lý trước đối số đều có thể được thực hiện trong Starlark thay vì tập lệnh. Đồng thời, tập lệnh có tính tự ghi lại nhiều hơn một chút vì sử dụng phần giữ chỗ tượng trưng (để thay thế) thay vì phần giữ chỗ dạng số (đối với đối số).

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

Ngoài ra, thay vì sử dụng thao tác mở rộng mẫu, bạn có thể đưa mẫu đó vào tệp .bzl dưới dạng một chuỗi và mở rộng tệp đó trong giai đoạn phân tích bằng phương thức str.format hoặc định dạng %.

Kiểm thử các tiện ích Starlark

Bạn có thể sử dụng khung unittest.bzl của Skylib để kiểm thử các hàm hiệu dụng (tức là các hàm không phải là macro cũng như không phải là cách triển khai quy tắc). Thay vì dùng thư viện analysistest của unittest.bzl, bạn có thể dùng unittest. Đối với các bộ kiểm thử như vậy, bạn có thể sử dụng hàm tiện lợi unittest.suite() để giảm bớt mã nguyên mẫu.

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

Để biết thêm ví dụ, hãy xem các quy trình kiểm thử của Skylib.