blob: 5957e6d059857843a222629c076f929d0aa92595 [file] [log] [blame] [view] [edit]
---
layout: documentation
title: Testing
---
# Testing
There are several different approaches to testing Skylark code in Bazel. This
page gathers the current best practices and frameworks by use case.
<!-- [TOC] -->
## For testing rules
[Skylib](https://github.com/bazelbuild/bazel-skylib) has a test framework called
[`unittest.bzl`](https://github.com/bazelbuild/bazel-skylib/blob/master/lib/unittest.bzl)
for checking the analysis-time behavior of rules, such as their actions and
providers. It is currently the best option for tests that need to access the
inner workings of Skylark rules.
Some caveats:
* Test assertions occur within the build, not a separate test runner process.
Targets that are created by the test must be named such that they do not
collide with targets from other tests or from the build. An error that occurs
during the test is seen by Bazel as a build breakage rather than a test
failure.
* It requires a fair amount of boilerplate to set up the rules under test and
the rules containing test assertions. This boilerplate may seem daunting at
first. It helps to [keep in mind](concepts.md#evaluation-model) which code
runs during the loading phase and which code runs during the analysis phase.
* It cannot easily test for expected failures.
The basic principle is to define a testing rule that depends on the
rule-under-test. This gives the testing rule access to the rule-under-tests
providers. There is experimental support for passing along action information
in the form of an additional provider.
The testing rules implementation function carries out assertions. If there are
any failures, these are not raised immediately by calling `fail()` (which would
trigger an analysis-time build error), but rather by storing the errors in a
generated script that fails at test execution time.
See below for a minimal toy example, followed by an example that checks actions.
### Minimal example
`//mypkg/BUILD`:
```python
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()
```
`//mypkg/myrules.bzl`:
```python
MyInfo = provider()
def _myrule_impl(ctx):
"""Rule that just generates a file and returns a provider."""
ctx.actions.write(ctx.outputs.out, "abc")
return [MyInfo(val="some value", out=ctx.outputs.out)]
myrule = rule(
implementation = _myrule_impl,
outputs = {"out": "%{name}.out"},
)
```
`//mypkg/myrules_test.bzl`:
```python
load("@bazel_skylib//:lib.bzl", "asserts", "unittest")
load(":myrules.bzl", "myrule", "MyInfo")
# ==== Check the provider contents ====
def _provider_contents_test_impl(ctx):
# Analysis-time test logic; place assertions here. Always begins with begin()
# and ends with end(). If you forget to call end(), you will get an error
# about the test result file not having a generating action.
env = unittest.begin(ctx)
asserts.equals(env, "some value", ctx.attr.dep[MyInfo].val)
# You can also use keyword arguments for readability if you prefer.
asserts.equals(env,
expected="some value",
actual=ctx.attr.dep[MyInfo].val)
unittest.end(env)
# Create the testing rule to wrap the test logic. Note that this must be bound
# to a global variable due to restrictions on how rules can be defined. Also,
# its name must end with "_test".
provider_contents_test = unittest.make(_provider_contents_test_impl,
attrs={"dep": attr.label()})
# You can use a different attrs dict if you need to take in multiple rules for
# the same unit test, or if you need to test an aspect, or if you want to
# parameterize the assertions with different expected results.
# Macro to setup the test.
def test_provider_contents():
# Rule under test.
myrule(name = "provider_contents_subject")
# Testing rule.
provider_contents_test(name = "provider_contents",
dep = ":provider_contents_subject")
# 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():
# Call all test functions and wrap their targets in a suite.
test_provider_contents()
# ...
native.test_suite(
name = "myrules_test",
tests = [
":provider_contents",
# ...
],
)
```
The test can be run with `bazel test //mypkg:myrules_test`.
Aside from the initial `load()` statements, there are two main parts to the
file:
* The tests themselves, each of which consists of 1) an analysis-time
implementation function for the testing rule, 2) a declaration of the testing
rule via `unittest.make()`, and 3) a loading-time function (macro) for
declaring the rule-under-test (and its dependencies) and testing rule. If the
assertions do not change between test cases, 1) and 2) may be shared by
multiple test cases.
* The test suite function, which calls the loading-time functions for each test,
and declares a `test_suite` target bundling all tests together.
We recommend the following naming convention. Let `foo` stand for the part of
the test name that describes what the test is checking (`provider_contents` in
the above example). For example, a JUnit test method would be named `testFoo`.
Then:
* the loading-time function should should be named `test_foo`
(`test_provider_contents`)
* its testing rule type should be named `foo_test` (`provider_contents_test`)
* the label of the target of this rule type should be `foo`
(`provider_contents`)
* the implementation function for the testing rule should be named
`_foo_test_impl` (`_provider_contents_test_impl`)
* the labels of the targets of the rules under test and their dependencies
should be prefixed with `foo_` (`provider_contents_`)
Note that the labels of all targets can conflict with other labels in the same
BUILD package, so its helpful to use a unique name for the test.
### Actions example
To check that the `ctx.actions.write()` line works correctly, the above example
is modified as follows.
`//mypkg/myrules.bzl`:
```python
...
myrule = rule(
implementation = _myrule_impl,
outputs = {"out": "%{name}.out"},
# This enables the Actions provider for this rule.
_skylark_testable = True,
)
```
`//mypkg/myrules_test.bzl`:
```python
...
# ==== Check the emitted file_action ====
def _file_action_test_impl(ctx):
env = unittest.begin(ctx)
dep = ctx.attr.dep
# Retrieve the Actions provider.
actions = dep[Actions]
# Retrieve the generating action for the output file.
action = actions.by_file[dep.out]
# Check the content that is to be written by the action.
asserts.equals(env, action.content, "abc")
unittest.end(env)
file_action_test = unittest.make(_file_action_test_impl,
attrs={"dep": attr.label()})
def test_file_action():
myrule(name = "file_action_subject")
file_action_test(name = "file_action",
dep = ":file_action_subject")
...
def myrules_test_suite():
# Call all test functions and wrap their targets in a suite.
test_provider_contents()
test_file_action()
# ...
native.test_suite(
name = "myrules_test",
tests = [
":provider_contents",
":file_action",
# ...
]
),
```
The flag `"_skylark_testable = True"` is needed on any rule whose actions are to
be tested. This triggers the creation of the `Actions` provider. (The leading
underscore is because this API is still experimental.) The test logic for
actions makes use of the following API.
### Actions API
The [`Actions`](lib/globals.html#Actions) provider is retrieved like any other
(non-legacy) provider:
```python
ctx.attr.foo[Actions]
```
The returned object has a single field, `by_file`, which holds a dictionary
mapping each of the rules output files to its generating action. (Actions that
do not have output files, in particular those generated by
`ctx.actions.do_nothing()`, cannot be retrieved.)
The interface of the actions stored in the `by_file` map is documented
[here](lib/Action.html).
Finally, there is support for testing helper functions that are not rules, but
that take in a rules `ctx` in order to create actions on it. Use
`ctx.created_actions()` to get an `Actions` provider that has information about
all actions created on `ctx` up to the point that this function was called. For
this to work, the testing rule itself must have `"_skylark_testable=True"` set.
Testing rules created using `unittest.make()` automatically have this flag set.
## For validating artifacts
There are two main ways of checking that your generated files are correct: You
can write a test script in shell, Python, or another language, and create a
target of the appropriate `*_test` rule type; or you can use a specialized rule
for the kind of test you want to perform.
### Using a test target
The most straightforward way to validate an artifact is to write a script and
add a `*_test` target to your BUILD file. The specific artifacts you want to
check should be data dependencies of this target. If your validation logic is
reusable for multiple tests, it should be a script that takes command line
arguments that are controlled by the test targets `args` attribute. Heres an
example that validates that the output of `myrule` from above is `"abc"`.
`//mypkg/myrule_validator.sh`:
```bash
if [ "$(cat $1)" = "abc" ]; then
echo "Passed"
exit 0
else
echo "Failed"
exit 1
fi
```
`//mypkg/BUILD`:
```python
...
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"],
)
```
### Using a custom rule
A more complicated alternative is to write the shell script as a template that
gets instantiated by a new Skylark rule. This involves more indirection and
Skylark logic, but leads to cleaner BUILD files. As a side-benefit, any argument
preprocessing can be done in Skylark instead of the script, and the script is
slightly more self-documenting since it uses symbolic placeholders (for
substitutions) instead of numeric ones (for arguments).
`//mypkg/myrule_validator.sh.template`:
```bash
if [ "$(cat %TARGET%)" = "abc" ]; then
echo "Passed"
exit 0
else
echo "Failed"
exit 1
fi
```
`//mypkg/myrule_validation.bzl`:
```python
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(single_file=True),
# We 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(single_file=True,
default=Label("//mypkg:myrule_validator"))},
test = True,
)
```
`//mypkg/BUILD`:
```python
...
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 we 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 we want to override it).
myrule_validation_test(
name = "validate_mytarget",
target = ":mytarget",
)
```
Alternatively, instead of using a template expansion action, we could have
inlined the template into the .bzl file as a string and expanded it during the
analysis phase using the `str.format` method or `%`-formatting.
## For testing Skylark utilities
The same framework that was used to test rules can also be used to test utility
functions (i.e., functions that are neither macros nor rule implementations).
There is no need to pass an `attrs` argument to `unittest.make()`, and there is
no special loading-time setup code to instantiate any rules-under-test. The
convenience function `unittest.suite()` can be used to reduce boilerplate in
this case.
`//mypkg/BUILD`:
```python
load(":myhelpers_test.bzl", "myhelpers_test_suite")
myhelpers_test_suite()
```
`//mypkg/myhelpers.bzl`:
```python
def myhelper():
return "abc"
```
`//mypkg/myhelpers_test.bzl`:
```python
load("@bazel_skylib//:lib.bzl", "asserts", "unittest")
load(":myhelpers.bzl", "myhelper")
def _myhelper_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(env, "abc", myhelper())
unittest.end(env)
myhelper_test = unittest.make(_myhelper_test_impl)
# No need for a test_myhelper() setup function.
def myhelpers_test_suite():
# unittest.suite() takes care of instantiating the testing rules and creating
# a test_suite.
unittest.suite(
"myhelpers_tests",
myhelper_test,
# ...
)
```
For more examples, see Skylibs own [tests](https://github.com/bazelbuild/bazel-skylib/blob/master/tests/BUILD).
This can also be used when the utility function takes in a rules `ctx` object
as a parameter. If the behavior of the utility function requires that the rule
be defined in a certain way, you may have to pass in an `attrs` parameter to
`unittest.make()` after all, or you may have to declare the rule manually using
`rule()`. To test helpers that create actions, make the unit test rule set
`"_skylark_testable=True"` (if it is not created via `unittest.make()`) and
write assertions on the result of `ctx.created_actions()`, as described above.