Testes

Reportar um problema Ver a fonte Nightly · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Há várias abordagens diferentes para testar o código Starlark no Bazel. Esta página reúne as práticas recomendadas e estruturas atuais por caso de uso.

Como testar regras

A Skylib tem uma estrutura de teste chamada unittest.bzl para verificar o comportamento das regras no momento da análise, como ações e provedores. Esses testes são chamados de "testes de análise" e são a melhor opção para testar o funcionamento interno das regras.

Algumas advertências:

  • As declarações de teste ocorrem no build, não em um processo separado de execução de testes. Os destinos criados pelo teste precisam ser nomeados de forma que não entrem em conflito com destinos de outros testes ou da build. Um erro que ocorre durante o teste é visto pelo Bazel como uma quebra de build, e não como uma falha de teste.

  • É preciso uma quantidade razoável de clichê para configurar as regras em teste e as regras que contêm declarações de teste. Esse modelo pode parecer assustador a princípio. É importante lembrar que as macros são avaliadas e os destinos gerados durante a fase de carregamento, enquanto as funções de implementação de regras não são executadas até mais tarde, durante a fase de análise.

  • Os testes de análise são pequenos e leves. Alguns recursos da estrutura de teste de análise são restritos à verificação de destinos com um número máximo de dependências transitivas (atualmente 500). Isso ocorre devido às implicações de performance do uso desses recursos com testes maiores.

O princípio básico é definir uma regra de teste que dependa da regra em teste. Isso dá à regra de teste acesso aos provedores da regra em teste.

A função de implementação da regra de teste realiza asserções. Se houver falhas, elas não serão geradas imediatamente ao chamar fail() (o que acionaria um erro de build no momento da análise), mas sim armazenando os erros em um script gerado que falha no momento da execução do teste.

Confira abaixo um exemplo mínimo e outro que verifica ações.

Exemplo mínimo

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

O teste pode ser executado com bazel test //mypkg:myrules_test.

Além das instruções load() iniciais, há duas partes principais no arquivo:

  • Os testes em si, cada um consistindo de 1) uma função de implementação no momento da análise para a regra de teste, 2) uma declaração da regra de teste via analysistest.make() e 3) uma função (macro) no momento do carregamento para declarar a regra em teste (e suas dependências) e a regra de teste. Se as asserções não mudarem entre os casos de teste, 1) e 2) poderão ser compartilhados por vários casos de teste.

  • A função do pacote de testes, que chama as funções de tempo de carregamento para cada teste e declara um test_suite de destino que agrupa todos os testes.

Para manter a consistência, siga a convenção de nomenclatura recomendada: deixe foo representar a parte do nome do teste que descreve o que ele está verificando (provider_contents no exemplo acima). Por exemplo, um método de teste do JUnit seria chamado de testFoo.

Em seguida:

  • A macro que gera o teste e o destino em teste deve ser chamada de _test_foo (_test_provider_contents).

  • O tipo de regra de teste deve ser chamado de foo_test (provider_contents_test).

  • o rótulo do destino desse tipo de regra precisa ser foo_test (provider_contents_test)

  • A função de implementação da regra de teste precisa ser chamada _foo_test_impl (_provider_contents_test_impl)

  • Os rótulos dos destinos das regras em teste e das dependências precisam ter o prefixo foo_ (provider_contents_).

Os rótulos de todos os destinos podem entrar em conflito com outros rótulos no mesmo pacote BUILD. Por isso, é útil usar um nome exclusivo para o teste.

Teste de falha

Pode ser útil verificar se uma regra falha com determinadas entradas ou em determinado estado. Isso pode ser feito usando a estrutura de teste de análise:

A regra de teste criada com analysistest.make precisa especificar expect_failure:

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

A implementação da regra de teste precisa fazer asserções sobre a natureza da falha que ocorreu (especificamente, a mensagem de falha):

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

Além disso, verifique se o destino em teste está marcado especificamente como "manual". Sem isso, a criação de todos os destinos no pacote usando :all vai resultar em uma criação do destino com falha intencional e vai mostrar uma falha de build. Com "manual", o destino em teste será criado apenas se especificado explicitamente ou como uma dependência de um destino não manual (como sua regra de teste):

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.

Como verificar ações registradas

Talvez você queira escrever testes que façam declarações sobre as ações registradas pela sua regra, por exemplo, usando ctx.actions.run(). Isso pode ser feito na função de implementação da regra de teste de análise. Um exemplo:

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)

Observe que analysistest.target_actions(env) retorna uma lista de objetos Action que representam ações registradas pelo destino em teste.

Verificar o comportamento da regra em diferentes flags

Talvez seja necessário verificar se a regra real se comporta de uma determinada maneira, considerando determinadas flags de build. Por exemplo, sua regra pode se comportar de maneira diferente se um usuário especificar:

bazel build //mypkg:real_target -c opt

versus

bazel build //mypkg:real_target -c dbg

À primeira vista, isso pode ser feito testando o destino em teste usando as flags de build desejadas:

bazel test //mypkg:myrules_test -c opt

Mas, assim, fica impossível para o pacote de testes conter simultaneamente um teste que verifique o comportamento da regra em -c opt e outro que verifique o comportamento da regra em -c dbg. Os dois testes não podem ser executados no mesmo build.

Isso pode ser resolvido especificando as flags de build desejadas ao definir a regra de teste:

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

Normalmente, um destino em teste é analisado com base nas flags de build atuais. Especificar config_settings substitui os valores das opções de linha de comando especificadas. As opções não especificadas vão manter os valores da linha de comando real.

No dicionário config_settings especificado, as flags de linha de comando precisam ter um prefixo com um valor de marcador especial //command_line_option:, conforme mostrado acima.

Como validar artefatos

As principais maneiras de verificar se os arquivos gerados estão corretos são:

  • É possível escrever um script de teste em shell, Python ou outra linguagem e criar um destino do tipo de regra *_test apropriado.

  • Você pode usar uma regra especializada para o tipo de teste que quer realizar.

Como usar um destino de teste

A maneira mais simples de validar um artefato é escrever um script e adicionar uma meta *_test ao arquivo BUILD. Os artefatos específicos que você quer verificar precisam ser dependências de dados dessa meta. Se a lógica de validação for reutilizável em vários testes, ela precisará ser um script que receba argumentos de linha de comando controlados pelo atributo args do destino do teste. Este é um exemplo que valida se a saída de myrule acima é "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"],
)

Usar uma regra personalizada

Uma alternativa mais complicada é escrever o script shell como um modelo que é instanciado por uma nova regra. Isso envolve mais indireção e lógica do Starlark, mas resulta em arquivos BUILD mais limpos. Como um benefício adicional, qualquer pré-processamento de argumentos pode ser feito em Starlark em vez do script, e o script é um pouco mais autodocumentado, já que usa marcadores de posição simbólicos (para substituições) em vez de numéricos (para argumentos).

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

Como alternativa, em vez de usar uma ação de expansão de modelo, você poderia ter inserido o modelo no arquivo .bzl como uma string e o expandido durante a fase de análise usando o método str.format ou a formatação %.

Como testar utilitários do Starlark

O framework unittest.bzl da Skylib pode ser usado para testar funções utilitárias (ou seja, funções que não são macros nem implementações de regras). Em vez de usar a biblioteca analysistest do unittest.bzl, é possível usar o unittest. Para esses conjuntos de testes, a função de conveniência unittest.suite() pode ser usada para reduzir o boilerplate.

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

Para mais exemplos, consulte os testes da Skylib.