Kiểm thử

Báo cáo vấn đề Xem nguồn Nightly · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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

Kiểm thử quy tắc

Skylib có một khung thử nghiệm có tên là unittest.bzl để kiểm tra hành vi trong thời gian phân tích của các quy tắc, chẳng hạn như hành động và nhà cung cấp của các quy tắc đó. Những kiểm thử như vậy được gọi là "kiểm thử phân tích" và hiện là lựa 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ố điều cần lưu ý:

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

  • Bạn cần một lượng mã nguyên mẫu hợp lý để thiết lập các quy tắc đang được kiểm thử và các quy tắc chứa các câu khẳng định kiểm thử. Mẫu này có vẻ khó hiểu lúc đầu. Bạn nên lưu ý rằng các macro được đánh giá và 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 không chạy cho đến sau này, trong giai đoạn phân tích.

  • Các kiểm thử phân tích được thiết kế để có quy mô khá nhỏ và đơn giản. Một số tính năng của khung kiểm thử phân tích bị hạn chế để 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 tác động đến hiệu suất của việc sử dụng các tính năng này với các kiểm thử lớn hơn.

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

Hàm triển khai của quy tắc kiểm thử thực hiện các câu khẳng định. Nếu có bất kỳ lỗi nào, những lỗi này sẽ không được fail() gọi ngay lập tức (điều này sẽ kích hoạt lỗi bản dựng tại thời điểm phân tích), mà được lưu trữ trong một tập lệnh được tạo và 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, sau đó là ví dụ kiểm tra các thao tác.

Ví dụ tối giản

//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òn có 2 phần chính:

  • Bản thân các kiểm thử, mỗi kiểm thử bao gồm 1) một hàm triển khai thời gian phân tích cho quy tắc kiểm thử, 2) một khai báo về quy tắc kiểm thử thông qua analysistest.make() và 3) một hàm thời gian tải (macro) để khai báo quy tắc đang kiểm thử (và các phần phụ thuộc của quy tắc đó) và quy tắc kiểm thử. Nếu các câu khẳng đị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ử dùng chung.

  • Hàm bộ kiểm thử, gọi các hàm thời gian tải cho từng kiểm thử và khai báo một mục tiêu test_suite liên kết tất cả các kiểm thử với nhau.

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

Sau đó:

  • macro tạo phép kiểm thử và mục tiêu trong phép kiểm thử phải được đặt tên là _test_foo (_test_provider_contents)

  • loại quy tắc kiểm thử của nó phải được đặt tên là foo_test (provider_contents_test)

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

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

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

Xin lưu ý rằng nhãn của tất cả các mục tiêu có thể xung đột với các nhãn khác trong cùng một gói BUILD, vì vậy, bạn nên sử dụng một tên riêng cho bài kiểm thử.

Kiểm thử lỗi

Bạn nên xác minh rằng một quy tắc không thành công khi có một số dữ liệu đầu vào hoặc ở một số trạng thái nhất định. Bạn có thể thực hiện việc này bằng cách sử dụng khung kiểm thử 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,
)

Việc triển khai quy tắc kiểm thử phải đưa ra các khẳng định 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 được kiểm thử được gắn thẻ "thủ công" một cách cụ thể. Nếu không có đ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 mục tiêu cố ý thất bại và sẽ cho thấy lỗi khi tạo. Với "manual", mục tiêu đang được kiểm thử sẽ chỉ được tạo nếu được chỉ định rõ ràng hoặc là một phần phụ thuộc của mục tiêu không phải là mục tiêu thủ công (chẳng hạn như quy tắc kiểm thử):

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 thao tác đã đăng ký

Bạn có thể muốn viết các bài kiểm thử đưa ra nhận định về những hành động mà quy tắc của bạn đăng ký, ví dụ: 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)

Xin 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 thao tác do mục tiêu đang được kiểm thử đăng ký.

Xác minh hành vi của quy tắc trong các cờ khác nhau

Bạn có thể muốn xác minh xem quy tắc thực tế của mình có hoạt động theo một cách nhất định hay không khi có 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 khác đi nếu người dùng chỉ định:

bazel build //mypkg:real_target -c opt

đấu 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ác cờ bản dựng mong muốn:

bazel test //mypkg:myrules_test -c opt

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

Bạn có thể giải quyết vấn đề này bằng cách chỉ định các 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 được kiểm thử sẽ được phân tích dựa trên các cờ bản dựng hiện tại. Việc chỉ định config_settings sẽ ghi đè các giá trị của các lựa chọn dòng lệnh được chỉ định. (Mọi lựa chọn không được chỉ định sẽ giữ lại giá trị của chúng từ dòng lệnh thực tế).

Trong từ điển config_settings được chỉ định, các 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

Các cách chính để kiểm tra xem tệp đã tạo có chính xác hay không là:

  • Bạn có thể viết một tập lệnh kiểm thử trong shell, Python hoặc một ngôn ngữ khác và tạo một mục tiêu thuộc 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 một cấu phần phần mềm là viết một tập lệnh và thêm mục tiêu *_test vào tệp BUILD của bạn. Các cấu phần phần mềm cụ thể mà bạn muốn kiểm tra phải là các 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ể dùng lại cho nhiều 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. Sau đây là một ví dụ xác thực rằng đầu ra của myrule từ phía 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 lựa chọn phức tạp hơn là viết tập lệnh shell dưới dạng một mẫu được khởi tạo bằng một quy tắc mới. Điều này liên quan đến nhiều logic gián tiếp và Starlark hơn, nhưng dẫn đến các tệp BUILD rõ ràng hơn. Một lợi ích phụ là bạn có thể thực hiện mọi hoạt động tiền xử lý đối số trong Starlark thay vì tập lệnh. Tập lệnh này có khả năng tự ghi lại tài liệu hơn một chút vì sử dụng các phần giữ chỗ mang tính biểu tượng (để thay thế) thay vì các phần giữ chỗ bằng số (cho đố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 mẫu đó 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ể dùng khung unittest.bzl của Skylib để kiểm thử các hàm tiện ích (tức là các hàm không phải là macro cũng không phải là các hàm triển khai quy tắc). Bạn có thể sử dụng unittest thay vì thư viện analysistest của unittest.bzl. Đối với những bộ kiểm thử như vậy, bạn có thể dùng hàm tiện ích unittest.suite() để giảm 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")

Để xem thêm ví dụ, hãy xem các kiểm thử của Skylib.