BazelCon 2022는 11월 16~17일에 뉴욕과 온라인에서 개최됩니다.
지금 등록하기

테스트 중

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

Bazel에서 Starlark 코드를 테스트하는 방법에는 여러 가지가 있습니다. 이 페이지에서는 사용 사례별로 최신 권장사항과 프레임워크를 수집합니다.

규칙 테스트

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() 문 외에도 다음 두 가지 파일이 파일에 포함되어 있습니다.

  • 테스트 자체는 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'을 사용하면 타겟은 명시적으로 지정되거나 수동이 아닌 타겟 (예: 테스트 규칙)의 종속 항목으로만 빌드됩니다.

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

vs.

bazel build //mypkg:real_target -c dbg

언뜻 보기에는 원하는 빌드 플래그를 사용하여 테스트 중인 타겟을 테스트하여 이 작업을 수행할 수 있습니다.

bazel test //mypkg:myrules_test -c opt

그러나 그러면 테스트 모음이 -c opt에서 규칙 동작을 확인하는 테스트와 -c dbg에서 규칙 동작을 확인하는 다른 테스트를 동시에 포함할 수 없게 됩니다. 두 테스트 모두 동일한 빌드에서 실행할 수 없습니다.

테스트 규칙을 정의할 때 원하는 빌드 플래그를 지정하면 이 문제를 해결할 수 있습니다.

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

일반적으로 테스트 중인 타겟은 현재 빌드 플래그를 고려하여 분석됩니다. config_settings를 지정하면 지정된 명령줄 옵션의 값이 재정의됩니다. 지정하지 않은 옵션은 실제 명령줄에 있는 값을 유지합니다.

지정된 config_settings 사전에서 위와 같이 명령줄 플래그에는 특수 자리표시자 값 //command_line_option:를 붙여야 합니다.

아티팩트 검증

생성된 파일이 올바른지 확인하는 주요 방법은 다음과 같습니다.

  • 셸, Python 또는 다른 언어로 테스트 스크립트를 작성하고 적절한 *_test 규칙 유형의 대상을 만들 수 있습니다.

  • 실행하려는 테스트 종류에 특수한 규칙을 사용할 수 있습니다.

테스트 대상 사용

아티팩트의 유효성을 검사하는 가장 간단한 방법은 스크립트를 작성하고 BUILD 파일에 *_test 대상을 추가하는 것입니다. 확인하려는 특정 아티팩트는 이 대상의 데이터 종속 항목이어야 합니다. 검증 로직이 여러 테스트에 재사용 가능한 경우 테스트 대상의 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 유틸리티 테스트

Skylibunittest.bzl 프레임워크는 유틸리티 함수 (즉, 매크로 또는 규칙 구현이 아닌 함수)를 테스트하는 데 사용할 수 있습니다. 참고). unittest.bzlanalysistest 라이브러리를 사용하는 대신 unittest를 사용할 수 있습니다. 이러한 테스트 모음의 경우 편의 함수 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의 자체 테스트를 참조하세요.