Kiểm thử

Báo cáo vấn đề Xem nguồn Hằng đêm · 7.3 · 7.2 · 7.1 · 7 · 6,5

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

Kiểm thử quy tắc

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ư các hành động và hành động Google Cloud. Những thử nghiệm như vậy được gọi là "thử nghiệm phân tích" và hiện là người dùng 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 ý:

  • 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 phép kiểm thử tạo phải được đặt tên sao cho chúng không va chạm với các mục tiêu từ kiểm thử khác hoặc từ bản dựng. Một lỗi xảy ra trong quá trình kiểm thử và thấy Bazel là một sự cố gây vỡ bản dựng chứ không phải là kiểm thử không thành công.

  • Phương pháp này cần một lượng lớn các mã nguyên mẫu để thiết lập các quy tắc cần kiểm thử và các quy tắc chứa câu nhận định kiểm thử. Mã nguyên mẫu này có vẻ khó khăn ở đầu tiê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 quy tắc các chức năng triển khai không chạy cho đến sau này, trong giai đoạn phân tích.

  • Thử nghiệm phân tích có quy mô tương đối nhỏ và gọn nhẹ. Một số các tính năng của khung thử nghiệm phân tích chỉ giới hạn ở việc 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 ngụ ý về hiệu suất khi sử dụng các tính năng này với kiểm thử.

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 dưới kiểm thử. Việc này cho phép quy tắc kiểm thử truy cập vào Google Cloud.

Chức năng triển khai của quy tắc kiểm thử tiến hành xác nhận. Nếu có bất kỳ lỗi nào, những lỗi này sẽ không được đưa ra ngay lập tức bằng cách gọi fail() (lệnh này sẽ kích hoạt lỗi bản dựng tại thời điểm phân tích), mà bằng cách lưu trữ lỗi trong một tạo tập lệnh 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 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 chương trình kiểm thử bằng bazel test //mypkg:myrules_test.

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

  • Bản thân các kiểm thử, mỗi kiểm thử bao gồm 1) thời gian phân tích chức năng triển khai 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ử dưới mức (và các phần phụ thuộc của quy tắc đó) cũng như quy trình kiểm thử . Nếu câu nhận định không thay đổi giữa các trường hợp kiểm thử, 1) và 2) có thể là được nhiều trường hợp kiểm thử dùng chung.

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

Để đảm bảo tính nhất quán, hãy làm theo quy ước đặt tên được đề xuất: Hãy dùng foo để đạ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 kiểm thử JUnit sẽ được đặt tên là testFoo.

Sau đó:

  • nên macro tạo thử nghiệm và mục tiêu đang được thử nghiệm có tên _test_foo (_test_provider_contents)

  • loại quy tắc kiểm thử của nó sẽ đượ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 cho quy tắc kiểm thử phải được đặt tên _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à các phần phụ thuộc của chúng phải có tiền tố là foo_ (provider_contents_)

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 XÂY DỰNG gói, vì vậy, bạn nên sử dụng tên riêng biệt cho chương trình kiểm thử.

Không kiểm thử được

Có thể hữu ích khi xác minh rằng một quy tắc không thành công dựa vào các thông tin đầu vào nhất định hoặc trong một số trạng thái. 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 nhận đị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 thử nghiệm của bạn được gắn thẻ cụ thể là "thủ công". Nếu không thực hiện điều này, việc tạo tất cả mục tiêu trong gói của bạn bằng cách sử dụng :all sẽ dẫn đến bản dựng của mục tiêu cố tình không đạt được và sẽ có lỗi bản dựng. Bằng "thủ công", mục tiêu đang được thử nghiệm của bạn sẽ chỉ xây dựng nếu được chỉ định rõ ràng hoặc như 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ử):

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.

Đang xác minh các thao tác đã đăng ký

Có thể bạn sẽ cần viết các chương trình kiểm thử đưa ra khẳng định về những hành động mà thanh ghi quy tắc, 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 chức năng 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 hành động do mục tiêu đang được thử nghiệm.

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

Bạn có thể muốn xác minh quy tắc thực của mình hoạt động theo cách nhất định dựa trên cấu trúc cờ. 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

đấu với

bazel build //mypkg:real_target -c dbg

Nhìn chung, điều này có thể được thực hiện 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

Nhưng sau đó, bộ thử nghiệm của bạn không thể đồng thời chứa để 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. Không thể chạy cả hai thử nghiệm 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ờ bản dựng mong muốn khi xác định quy trình kiểm thử quy tắc:

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

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

Trong từ điển config_settings đã chỉ định, cờ dòng lệnh phải có tiền tố là một giá trị phần giữ chỗ đặc biệt //command_line_option: (như trong ví dụ này) ở trên.

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

Sau đây là một số cách chính để kiểm tra xem tệp đã tạo có chính xác hay không:

  • Bạn có thể viết một tập lệnh kiểm thử bằng shell, Python hoặc một 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 quy tắc chuyên biệt cho loại thử nghiệm mà mình muốn thực hiện.

Sử dụng mục tiêu thử nghiệm

Cách đơn giản nhất để xác thực 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ác cấu phần phần mềm cụ thể mà bạn muốn nên kiểm tra 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 là có thể sử dụng lại cho nhiều thử nghiệm, nó phải là một tập lệnh nhận dòng lệnh các đối số 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 kết quả 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 dưới dạng mẫu được tạo thực thể bởi một quy tắc mới. Việc này cần có nhiều thao tác gián tiếp hơn và Starlark logic, nhưng sẽ dẫn đến tệp BUILD rõ ràng hơn. Bất kỳ đối số nào cũng là lợi ích phụ trước khi xử lý có thể được thực hiện trong Starlark thay vì tập lệnh và kịch bản tự ghi chép nhiều hơn một chút vì công cụ này sử dụng phần giữ chỗ tượng trưng (cho ký tự thay thế) thay vì ký tự 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 hành động mở rộng mẫu, bạn có thể dùng đư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 cách sử dụng phương thức str.format hoặc định dạng %.

Kiểm thử tiện ích Starlark

của Skylib unittest.bzl khung có thể được sử dụng để kiểm thử các hàm hiệu dụng (nghĩa là các hàm cả macro lẫn triển khai quy tắc). Thay vì sử dụng đoạn mã của unittest.bzl Thư viện analysistest, unittest có thể được sử dụng. Đối với các bộ kiểm thử như vậy, có thể 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 kiểm thử riêng của Skylib.