Skip to content

Commit 6eeefb3

Browse files
rickeylevBlaze Rules Copybara
authored andcommitted
feat: Support unit test (non-analysis test) types of tests in rules_testing
This adds support to write tests of general Starlark code that doesn't require analysis phase information. While this was possible to do with enough work, it's a bit of boiler plate to do so. This moves that boiler plate into rules_testing, so that users just need to define their verification function. Also makes `test_suite` able to handle both basic tests (ones that are only an implementation function, like most unit tests) and regular tests (tests that require a loading-phase setup process, like analyss tests). Also creates a best practices doc. Some of the same advice pertains across test suite, unit test, and analysis test. Closes #37 PiperOrigin-RevId: 539743721
1 parent 61968e3 commit 6eeefb3

File tree

12 files changed

+584
-230
lines changed

12 files changed

+584
-230
lines changed

docs/source/best_practices.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Best Practices
2+
3+
Here we collection tips and techniques for keeping your tests maintainable and
4+
avoiding common pitfalls.
5+
6+
### Put each suite of tests in its own sub-package
7+
8+
It's recommended to put a given suite of unit tests in their own sub-package
9+
(directory with a BUILD file). This is because the names of your test functions
10+
become target names in the BUILD file, which makes it easier to create name
11+
conflicts. By moving them into their own package, you don't have to worry about
12+
unit test function names in one `.bzl` file conflicting with names in another.
13+
14+
### Give test functions private names
15+
16+
It's recommended to give test functions private names, i.e. start with a leading
17+
underscore. This is because if you forget to add a test to the list of tests (an
18+
easy mistake to make in a file with many tests), the test won't run, and it'll
19+
appear as if everything is OK. By using a leading underscore, tools like
20+
buildifier can detect the unused private function and will warn you that it's
21+
unused, preventing you from accidentally forgetting it.

docs/source/test_suite.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Test suites
2+
3+
The `test_suite` macro is a front-end for easily instantiating groups of
4+
Starlark tests. It can handle both analysis tests and unit tests. Under the
5+
hood, each test is its own target with an aggregating `native.test_suite`
6+
for the group of tests.
7+
8+
## Basic tests
9+
10+
Basic tests are tests that don't require any custom setup or attributes. This is
11+
the common case for tests of utility code that doesn't interact with objects
12+
only available to rules (e.g. Targets). These tests are created using
13+
`unit_test`.
14+
15+
To write such a test, simply write a `unit_test` compatible function (one that
16+
accepts `env`) and pass it to `test_suite.basic_tests`.
17+
18+
```starlark
19+
# BUILD
20+
21+
load(":my_tests.bzl", "my_test_suite")
22+
load("@rules_testing//lib:test_suite.bzl", "test_suite")
23+
24+
my_test_suite(name = "my_tests")
25+
26+
# my_tests.bzl
27+
28+
def _foo_test(env):
29+
env.expect.that_str(...).equals(...)
30+
31+
def my_test_suite(name):
32+
test_suite(
33+
name = name,
34+
basic_tests = [
35+
_foo_test,
36+
]
37+
)
38+
```
39+
40+
Note that it isn't _required_ to write a custom test suite function, but doing
41+
so is preferred because it's uncommon for BUILD files to pass around function
42+
objects, and tools won't be confused by it.
43+
44+
## Regular tests
45+
46+
A regular test is a macro that acts as a setup function and is expected to
47+
create a target of the given name (which is added to the underlying test suite).
48+
49+
The setup function can perform arbitrary logic, but in the end, it's expected to
50+
call `unit_test` or `analysis_test` to create a target with the provided name.
51+
52+
If you're writing an `analysis_test`, then you're writing a regular test.
53+
54+
```starlark
55+
# my_tests.bzl
56+
def _foo_test(name):
57+
analysis_test(
58+
name = name,
59+
impl = _foo_test_impl,
60+
attrs = {"myattr": attr.string(default="default")}
61+
)
62+
63+
def _foo_test_impl(env):
64+
env.expect.that_str(...).equals(...)
65+
66+
def my_test_suite(name):
67+
test_suite(
68+
name = name,
69+
tests = [
70+
_foo_test,
71+
]
72+
)
73+
```
74+
75+
Note that a using a setup function with `unit_test` test is not required to
76+
define custom attributes; the above is just an example. If you want to define
77+
custom attributes for every test in a suite, the `test_kwargs` argument of
78+
`test_suite` can be used to pass additional arguments to all tests in the suite.

docs/source/unit_tests.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Unit tests
2+
3+
Unit tests are for Starlark code that isn't specific to analysis-phase or
4+
loading phase cases; usually utility code of some sort. Such tests typically
5+
don't require a rule `ctx` or instantiating other targets to verify the code
6+
under test.
7+
8+
To write such a test, simply write a function accepting `env` and pass it to
9+
`test_suite`. The test suite will pass your verification function to
10+
`unit_test()` for you.
11+
12+
```starlark
13+
# BUILD
14+
15+
load(":my_tests.bzl", "my_test_suite")
16+
load("@rules_testing//lib:test_suite.bzl", "test_suite")
17+
18+
my_test_suite(name = "my_tests")
19+
20+
# my_tests.bzl
21+
22+
def _foo_test(env):
23+
env.expect.that_str(...).equals(...)
24+
25+
def my_test_suite(name):
26+
test_suite(
27+
name = name,
28+
basic_tests = [
29+
_foo_test,
30+
]
31+
)
32+
```
33+
34+
Note that it isn't _required_ to write a custom test suite function, but doing
35+
so is preferred because it's uncommon for BUILD files to pass around function
36+
objects, and tools won't be confused by it.
37+
38+
## Customizing setup
39+
40+
If you want to customize the setup (loading phase) of a unit test, e.g. to add
41+
custom attributes, then you need to write in the same style as an analysis test:
42+
one function is a verification function, and another function performs setup and
43+
calls `unit_test()`, passing in the verification function.
44+
45+
Custom tests are like basic tests, except you can hook into the loading phase
46+
before the actual unit test is defined. Because you control the invocation of
47+
`unit_test`, you can e.g. define custom attributes specific to the test.
48+
49+
```starlark
50+
# my_tests.bzl
51+
def _foo_test(name):
52+
unit_test(
53+
name = name,
54+
impl = _foo_test_impl,
55+
attrs = {"myattr": attr.string(default="default")}
56+
)
57+
58+
def _foo_test_impl(env):
59+
env.expect.that_str(...).equals(...)
60+
61+
def my_test_suite(name):
62+
test_suite(
63+
name = name,
64+
custom_tests = [
65+
_foo_test,
66+
]
67+
)
68+
```
69+
70+
Note that a custom test is not required to define custom attributes; the above
71+
is just an example. If you want to define custom attributes for every test in a
72+
suite, the `test_kwargs` argument of `test_suite` can be used to pass additional
73+
arguments to all tests in the suite.

lib/BUILD

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
16+
load("//lib/private:util.bzl", "do_nothing")
1617

1718
licenses(["notice"])
1819

@@ -26,7 +27,10 @@ bzl_library(
2627
srcs = ["analysis_test.bzl"],
2728
visibility = ["//visibility:public"],
2829
deps = [
29-
"//lib:truth_bzl",
30+
":test_suite_bzl",
31+
":truth_bzl",
32+
"//lib/private:analysis_test_bzl",
33+
"//lib/private:util_bzl",
3034
],
3135
)
3236

@@ -57,6 +61,25 @@ bzl_library(
5761
],
5862
)
5963

64+
bzl_library(
65+
name = "unit_test_bzl",
66+
srcs = ["unit_test.bzl"],
67+
visibility = ["//visibility:public"],
68+
deps = [
69+
"//lib/private:analysis_test_bzl",
70+
],
71+
)
72+
73+
bzl_library(
74+
name = "test_suite_bzl",
75+
srcs = ["test_suite.bzl"],
76+
visibility = ["//visibility:public"],
77+
deps = [
78+
":unit_test_bzl",
79+
"//lib/private:util_bzl",
80+
],
81+
)
82+
6083
filegroup(
6184
name = "test_deps",
6285
testonly = True,
@@ -81,3 +104,9 @@ exports_files(
81104
"//docgen:__pkg__",
82105
],
83106
)
107+
108+
# Unit tests need some target because they're based upon analysis tests.
109+
do_nothing(
110+
name = "_stub_target_for_unit_tests",
111+
visibility = ["//visibility:public"],
112+
)

0 commit comments

Comments
 (0)