feat: implement bats test runner (#699)

diff --git a/MODULE.bazel b/MODULE.bazel
index e63fa5b..ad59fe2 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -22,7 +22,8 @@
 bazel_lib_toolchains.coreutils()
 bazel_lib_toolchains.tar()
 bazel_lib_toolchains.expand_template()
-use_repo(bazel_lib_toolchains, "bsd_tar_toolchains", "copy_directory_toolchains", "copy_to_directory_toolchains", "coreutils_toolchains", "expand_template_toolchains", "jq_toolchains", "yq_toolchains")
+bazel_lib_toolchains.bats()
+use_repo(bazel_lib_toolchains, "bats_toolchains", "bsd_tar_toolchains", "copy_directory_toolchains", "copy_to_directory_toolchains", "coreutils_toolchains", "expand_template_toolchains", "jq_toolchains", "yq_toolchains")
 
 register_toolchains(
     "@copy_directory_toolchains//:all",
@@ -31,6 +32,7 @@
     "@yq_toolchains//:all",
     "@coreutils_toolchains//:all",
     "@expand_template_toolchains//:all",
+    "@bats_toolchains//:all",
     # Expand bsd_tar_toolchains
     "@bsd_tar_toolchains//:linux_amd64_toolchain",
     "@bsd_tar_toolchains//:linux_arm64_toolchain",
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 5a0e479..ec54168 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -151,4 +151,9 @@
     bzl_library_target = "//lib:strings",
 )
 
+stardoc_with_diff_test(
+    name = "bats",
+    bzl_library_target = "//lib:bats",
+)
+
 update_docs()
diff --git a/docs/bats.md b/docs/bats.md
new file mode 100644
index 0000000..6de99e0
--- /dev/null
+++ b/docs/bats.md
@@ -0,0 +1,25 @@
+<!-- Generated with Stardoc: http://skydoc.bazel.build -->
+
+Bats test runner
+
+<a id="bats_test"></a>
+
+## bats_test
+
+<pre>
+bats_test(<a href="#bats_test-name">name</a>, <a href="#bats_test-data">data</a>, <a href="#bats_test-env">env</a>, <a href="#bats_test-srcs">srcs</a>)
+</pre>
+
+
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="bats_test-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="bats_test-data"></a>data |  Runtime dependencies of the test.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | <code>[]</code> |
+| <a id="bats_test-env"></a>env |  Environment variables of the action.<br><br>            Subject to [$(location)](https://bazel.build/reference/be/make-variables#predefined_label_variables)             and ["Make variable"](https://bazel.build/reference/be/make-variables) substitution.   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional | <code>{}</code> |
+| <a id="bats_test-srcs"></a>srcs |  Test files   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | <code>[]</code> |
+
+
diff --git a/docs/repositories.md b/docs/repositories.md
index 15e3d56..578fc40 100644
--- a/docs/repositories.md
+++ b/docs/repositories.md
@@ -29,6 +29,31 @@
 
 
 
+<a id="register_bats_toolchains"></a>
+
+## register_bats_toolchains
+
+<pre>
+register_bats_toolchains(<a href="#register_bats_toolchains-name">name</a>, <a href="#register_bats_toolchains-core_version">core_version</a>, <a href="#register_bats_toolchains-support_version">support_version</a>, <a href="#register_bats_toolchains-assert_version">assert_version</a>, <a href="#register_bats_toolchains-file_version">file_version</a>,
+                         <a href="#register_bats_toolchains-libraries">libraries</a>, <a href="#register_bats_toolchains-register">register</a>)
+</pre>
+
+Registers bats toolchain and repositories
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="register_bats_toolchains-name"></a>name |  override the prefix for the generated toolchain repositories   |  <code>"bats"</code> |
+| <a id="register_bats_toolchains-core_version"></a>core_version |  bats-core version to use   |  <code>"v1.10.0"</code> |
+| <a id="register_bats_toolchains-support_version"></a>support_version |  bats-support version to use   |  <code>"v0.3.0"</code> |
+| <a id="register_bats_toolchains-assert_version"></a>assert_version |  bats-assert version to use   |  <code>"v2.1.0"</code> |
+| <a id="register_bats_toolchains-file_version"></a>file_version |  bats-file version to use   |  <code>"v0.4.0"</code> |
+| <a id="register_bats_toolchains-libraries"></a>libraries |  additional labels for libraries   |  <code>[]</code> |
+| <a id="register_bats_toolchains-register"></a>register |  whether to call through to native.register_toolchains. Should be True for WORKSPACE users, but false when used under bzlmod extension   |  <code>True</code> |
+
+
 <a id="register_copy_directory_toolchains"></a>
 
 ## register_copy_directory_toolchains
diff --git a/e2e/smoke/BUILD.bazel b/e2e/smoke/BUILD.bazel
index cc2fd20..a41fee5 100644
--- a/e2e/smoke/BUILD.bazel
+++ b/e2e/smoke/BUILD.bazel
@@ -1,3 +1,4 @@
+load("@aspect_bazel_lib//lib:bats.bzl", "bats_test")
 load("@aspect_bazel_lib//lib:copy_directory.bzl", "copy_directory")
 load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
 load("@aspect_bazel_lib//lib:diff_test.bzl", "diff_test")
@@ -91,3 +92,10 @@
     srcs = [],
     mtree = [],
 )
+
+bats_test(
+    name = "bats",
+    srcs = [
+        "basic.bats",
+    ],
+)
diff --git a/e2e/smoke/basic.bats b/e2e/smoke/basic.bats
new file mode 100644
index 0000000..03d254c
--- /dev/null
+++ b/e2e/smoke/basic.bats
@@ -0,0 +1,8 @@
+bats_load_library "bats-support"
+bats_load_library "bats-assert"
+
+@test 'basic' {
+    run echo 'have'
+    assert_output 'have'
+}
+
diff --git a/internal_deps.bzl b/internal_deps.bzl
index ff25756..45d0f9c 100644
--- a/internal_deps.bzl
+++ b/internal_deps.bzl
@@ -4,7 +4,7 @@
 statement from these, that's a bug in our distribution.
 """
 
-load("//lib:repositories.bzl", "register_coreutils_toolchains", "register_jq_toolchains", "register_tar_toolchains", "register_yq_toolchains")
+load("//lib:repositories.bzl", "register_bats_toolchains", "register_coreutils_toolchains", "register_jq_toolchains", "register_tar_toolchains", "register_yq_toolchains")
 load("//lib:utils.bzl", http_archive = "maybe_http_archive")
 
 # buildifier: disable=unnamed-macro
@@ -72,3 +72,6 @@
     register_yq_toolchains()
     register_coreutils_toolchains()
     register_tar_toolchains()
+    register_bats_toolchains(
+        libraries = ["@aspect_bazel_lib//lib/tests/bats/bats-custom:custom"],
+    )
diff --git a/lib/BUILD.bazel b/lib/BUILD.bazel
index d689dee..0a56285 100644
--- a/lib/BUILD.bazel
+++ b/lib/BUILD.bazel
@@ -59,11 +59,16 @@
     name = "tar_toolchain_type",
 )
 
+toolchain_type(
+    name = "bats_toolchain_type",
+)
+
 bzl_library(
     name = "expand_make_vars",
     srcs = ["expand_make_vars.bzl"],
     deps = [
-        ":expand_template",
+        "//lib/private:expand_locations",
+        "//lib/private:expand_variables",
     ],
 )
 
@@ -235,6 +240,7 @@
     srcs = ["repositories.bzl"],
     deps = [
         ":utils",
+        "//lib/private:bats_toolchain",
         "//lib/private:copy_directory_toolchain",
         "//lib/private:copy_to_directory_toolchain",
         "//lib/private:coreutils_toolchain",
@@ -295,3 +301,9 @@
     srcs = ["windows_utils.bzl"],
     deps = ["//lib/private:paths"],
 )
+
+bzl_library(
+    name = "bats",
+    srcs = ["bats.bzl"],
+    deps = ["//lib/private:bats"],
+)
diff --git a/lib/bats.bzl b/lib/bats.bzl
new file mode 100644
index 0000000..fbc018b
--- /dev/null
+++ b/lib/bats.bzl
@@ -0,0 +1,5 @@
+"Bats test runner"
+
+load("//lib/private:bats.bzl", _bats_test = "bats_test")
+
+bats_test = _bats_test
diff --git a/lib/extensions.bzl b/lib/extensions.bzl
index d3c46dc..8ba94dc 100644
--- a/lib/extensions.bzl
+++ b/lib/extensions.bzl
@@ -2,6 +2,8 @@
 
 load(
     "@aspect_bazel_lib//lib:repositories.bzl",
+    "DEFAULT_BATS_CORE_VERSION",
+    "DEFAULT_BATS_REPOSITORY",
     "DEFAULT_COPY_DIRECTORY_REPOSITORY",
     "DEFAULT_COPY_TO_DIRECTORY_REPOSITORY",
     "DEFAULT_COREUTILS_REPOSITORY",
@@ -12,6 +14,7 @@
     "DEFAULT_TAR_REPOSITORY",
     "DEFAULT_YQ_REPOSITORY",
     "DEFAULT_YQ_VERSION",
+    "register_bats_toolchains",
     "register_copy_directory_toolchains",
     "register_copy_to_directory_toolchains",
     "register_coreutils_toolchains",
@@ -94,6 +97,15 @@
         get_version_fn = lambda attr: None,
     )
 
+    extension_utils.toolchain_repos_bfs(
+        mctx = mctx,
+        get_tag_fn = lambda tags: tags.bats,
+        toolchain_name = "bats",
+        default_repository = DEFAULT_BATS_REPOSITORY,
+        toolchain_repos_fn = lambda name, version: register_bats_toolchains(name = name, core_version = version, register = False),
+        get_version_fn = lambda attr: attr.core_version,
+    )
+
 toolchains = module_extension(
     implementation = _toolchains_extension_impl,
     tag_classes = {
@@ -104,5 +116,9 @@
         "coreutils": tag_class(attrs = {"name": attr.string(default = DEFAULT_COREUTILS_REPOSITORY), "version": attr.string(default = DEFAULT_COREUTILS_VERSION)}),
         "tar": tag_class(attrs = {"name": attr.string(default = DEFAULT_TAR_REPOSITORY)}),
         "expand_template": tag_class(attrs = {"name": attr.string(default = DEFAULT_EXPAND_TEMPLATE_REPOSITORY)}),
+        "bats": tag_class(attrs = {
+            "name": attr.string(default = DEFAULT_BATS_REPOSITORY),
+            "core_version": attr.string(default = DEFAULT_BATS_CORE_VERSION),
+        }),
     },
 )
diff --git a/lib/private/BUILD.bazel b/lib/private/BUILD.bazel
index bef7322..29fbccc 100644
--- a/lib/private/BUILD.bazel
+++ b/lib/private/BUILD.bazel
@@ -275,6 +275,23 @@
 )
 
 bzl_library(
+    name = "bats",
+    srcs = ["bats.bzl"],
+    visibility = ["//lib:__subpackages__"],
+    deps = [
+        ":expand_locations",
+        ":expand_variables",
+        "@aspect_bazel_lib//lib:paths",
+    ],
+)
+
+bzl_library(
+    name = "bats_toolchain",
+    srcs = ["bats_toolchain.bzl"],
+    visibility = ["//lib:__subpackages__"],
+)
+
+bzl_library(
     name = "copy_common",
     srcs = ["copy_common.bzl"],
     visibility = ["//lib:__subpackages__"],
diff --git a/lib/private/bats.bzl b/lib/private/bats.bzl
new file mode 100644
index 0000000..352319e
--- /dev/null
+++ b/lib/private/bats.bzl
@@ -0,0 +1,90 @@
+"bats_test"
+
+load("//lib:paths.bzl", "BASH_RLOCATION_FUNCTION", "to_rlocation_path")
+load(":expand_locations.bzl", "expand_locations")
+load(":expand_variables.bzl", "expand_variables")
+
+_RUNNER_TMPL = """#!/usr/bin/env bash
+set -o errexit -o nounset -o pipefail
+
+{BASH_RLOCATION_FUNCTION}
+
+readonly core_path="$(rlocation {core})"
+readonly bats="$core_path/bin/bats"
+readonly libs=( {libraries} )
+
+{envs}
+
+NEW_LIBS=()
+for lib in "${{libs[@]}}"; do
+    NEW_LIBS+=( $(cd "$(rlocation $lib)/.." && pwd) )
+done
+
+export BATS_LIB_PATH=$(
+    IFS=:
+    echo "${{NEW_LIBS[*]}}"
+)
+export BATS_TEST_TIMEOUT="$TEST_TIMEOUT"
+export BATS_TMPDIR="$TEST_TMPDIR"
+
+exec $bats {tests} $@
+"""
+
+_ENV_SET = """export {var}=\"{value}\""""
+
+def _bats_test_impl(ctx):
+    toolchain = ctx.toolchains["@aspect_bazel_lib//lib:bats_toolchain_type"]
+    batsinfo = toolchain.batsinfo
+
+    envs = []
+    for (key, value) in ctx.attr.env.items():
+        envs.append(_ENV_SET.format(
+            var = key,
+            value = " ".join([expand_variables(ctx, exp, attribute_name = "env") for exp in expand_locations(ctx, value, ctx.attr.data).split(" ")]),
+        ))
+
+    runner = ctx.actions.declare_file("%s_bats.sh" % ctx.label.name)
+    ctx.actions.write(
+        output = runner,
+        content = _RUNNER_TMPL.format(
+            core = to_rlocation_path(ctx, batsinfo.core),
+            libraries = " ".join([to_rlocation_path(ctx, lib) for lib in batsinfo.libraries]),
+            tests = " ".join([test.short_path for test in ctx.files.srcs]),
+            envs = "\n".join(envs),
+            BASH_RLOCATION_FUNCTION = BASH_RLOCATION_FUNCTION,
+        ),
+        is_executable = True,
+    )
+
+    runfiles = ctx.runfiles(ctx.files.srcs + ctx.files.data)
+    runfiles = runfiles.merge(toolchain.default.default_runfiles)
+    runfiles = runfiles.merge(ctx.attr._runfiles.default_runfiles)
+
+    return DefaultInfo(
+        executable = runner,
+        runfiles = runfiles,
+    )
+
+bats_test = rule(
+    implementation = _bats_test_impl,
+    attrs = {
+        "srcs": attr.label_list(
+            allow_files = [".bats"],
+            doc = "Test files",
+        ),
+        "data": attr.label_list(
+            allow_files = True,
+            doc = "Runtime dependencies of the test.",
+        ),
+        "env": attr.string_dict(
+            doc = """Environment variables of the action.
+
+            Subject to [$(location)](https://bazel.build/reference/be/make-variables#predefined_label_variables)
+            and ["Make variable"](https://bazel.build/reference/be/make-variables) substitution.
+            """,
+        ),
+        "_runfiles": attr.label(default = "@bazel_tools//tools/bash/runfiles"),
+    },
+    toolchains = ["@aspect_bazel_lib//lib:bats_toolchain_type"],
+    test = True,
+)
diff --git a/lib/private/bats_toolchain.bzl b/lib/private/bats_toolchain.bzl
new file mode 100644
index 0000000..7008674
--- /dev/null
+++ b/lib/private/bats_toolchain.bzl
@@ -0,0 +1,103 @@
+"Provide access to a bats executable"
+
+BATS_CORE_VERSIONS = {
+    "v1.10.0": "a1a9f7875aa4b6a9480ca384d5865f1ccf1b0b1faead6b47aa47d79709a5c5fd",
+}
+
+BATS_SUPPORT_VERSIONS = {
+    "v0.3.0": "7815237aafeb42ddcc1b8c698fc5808026d33317d8701d5ec2396e9634e2918f",
+}
+
+BATS_ASSERT_VERSIONS = {
+    "v2.1.0": "98ca3b685f8b8993e48ec057565e6e2abcc541034ed5b0e81f191505682037fd",
+}
+
+BATS_FILE_VERSIONS = {
+    "v0.4.0": "9b69043241f3af1c2d251f89b4fcafa5df3f05e97b89db18d7c9bdf5731bb27a",
+}
+
+BATS_CORE_TEMPLATE = """\
+load("@local_config_platform//:constraints.bzl", "HOST_CONSTRAINTS")
+load("@aspect_bazel_lib//lib/private:bats_toolchain.bzl", "bats_toolchain")
+load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
+
+copy_to_directory(
+    name = "core",
+    hardlink = "on",
+    srcs = glob([
+        "lib/**",
+        "libexec/**"
+    ]) + ["bin/bats"],
+    out = "bats-core",
+)
+
+bats_toolchain(
+    name = "toolchain",
+    core = ":core",
+    libraries = {libraries}
+)
+
+toolchain(
+    name = "bats_toolchain",
+    exec_compatible_with = HOST_CONSTRAINTS,
+    toolchain = ":toolchain",
+    toolchain_type = "@aspect_bazel_lib//lib:bats_toolchain_type",
+)
+"""
+
+BATS_LIBRARY_TEMPLATE = """\
+load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
+
+copy_to_directory(
+    name = "{name}",
+    hardlink = "on",
+    srcs = glob([
+        "src/**",
+        "load.bash",
+    ]),
+    out = "bats-{name}",
+    visibility = ["//visibility:public"]
+)
+"""
+
+BatsInfo = provider(
+    doc = "Provide info for executing bats",
+    fields = {
+        "core": "bats executable",
+        "libraries": "bats helper libraries",
+    },
+)
+
+def _bats_toolchain_impl(ctx):
+    core = ctx.file.core
+
+    default_info = DefaultInfo(
+        files = depset(ctx.files.core + ctx.files.libraries),
+        runfiles = ctx.runfiles(ctx.files.core + ctx.files.libraries),
+    )
+
+    batsinfo = BatsInfo(
+        core = core,
+        libraries = ctx.files.libraries,
+    )
+
+    # Export all the providers inside our ToolchainInfo
+    # so the resolved_toolchain rule can grab and re-export them.
+    toolchain_info = platform_common.ToolchainInfo(
+        batsinfo = batsinfo,
+        default = default_info,
+    )
+
+    return [toolchain_info, default_info]
+
+bats_toolchain = rule(
+    implementation = _bats_toolchain_impl,
+    attrs = {
+        "core": attr.label(
+            doc = "Label to the bats executable",
+            allow_single_file = True,
+            mandatory = True,
+        ),
+        "libraries": attr.label_list(),
+    },
+)
diff --git a/lib/repositories.bzl b/lib/repositories.bzl
index ec577db..b108961 100644
--- a/lib/repositories.bzl
+++ b/lib/repositories.bzl
@@ -1,6 +1,7 @@
 "Macros for loading dependencies and registering toolchains"
 
 load("//lib:utils.bzl", http_archive = "maybe_http_archive")
+load("//lib/private:bats_toolchain.bzl", "BATS_ASSERT_VERSIONS", "BATS_CORE_TEMPLATE", "BATS_CORE_VERSIONS", "BATS_FILE_VERSIONS", "BATS_LIBRARY_TEMPLATE", "BATS_SUPPORT_VERSIONS")
 load("//lib/private:copy_directory_toolchain.bzl", "COPY_DIRECTORY_PLATFORMS", "copy_directory_platform_repo", "copy_directory_toolchains_repo")
 load("//lib/private:copy_to_directory_toolchain.bzl", "COPY_TO_DIRECTORY_PLATFORMS", "copy_to_directory_platform_repo", "copy_to_directory_toolchains_repo")
 load("//lib/private:coreutils_toolchain.bzl", "COREUTILS_PLATFORMS", "coreutils_platform_repo", "coreutils_toolchains_repo", _DEFAULT_COREUTILS_VERSION = "DEFAULT_COREUTILS_VERSION")
@@ -103,6 +104,81 @@
         user_repository_name = name,
     )
 
+DEFAULT_BATS_REPOSITORY = "bats"
+
+DEFAULT_BATS_CORE_VERSION = "v1.10.0"
+DEFAULT_BATS_SUPPORT_VERSION = "v0.3.0"
+DEFAULT_BATS_ASSERT_VERSION = "v2.1.0"
+DEFAULT_BATS_FILE_VERSION = "v0.4.0"
+
+def register_bats_toolchains(
+        name = DEFAULT_BATS_REPOSITORY,
+        core_version = DEFAULT_BATS_CORE_VERSION,
+        support_version = DEFAULT_BATS_SUPPORT_VERSION,
+        assert_version = DEFAULT_BATS_ASSERT_VERSION,
+        file_version = DEFAULT_BATS_FILE_VERSION,
+        libraries = [],
+        register = True):
+    """Registers bats toolchain and repositories
+
+    Args:
+        name: override the prefix for the generated toolchain repositories
+        core_version: bats-core version to use
+        support_version: bats-support version to use
+        assert_version: bats-assert version to use
+        file_version: bats-file version to use
+        libraries: additional labels for libraries
+        register: whether to call through to native.register_toolchains.
+            Should be True for WORKSPACE users, but false when used under bzlmod extension
+    """
+
+    http_archive(
+        name = "%s_support" % name,
+        sha256 = BATS_SUPPORT_VERSIONS[support_version],
+        urls = [
+            "https://github.com/bats-core/bats-support/archive/{}.tar.gz".format(support_version),
+        ],
+        strip_prefix = "bats-support-{}".format(support_version.removeprefix("v")),
+        build_file_content = BATS_LIBRARY_TEMPLATE.format(name = "support"),
+    )
+
+    http_archive(
+        name = "%s_assert" % name,
+        sha256 = BATS_ASSERT_VERSIONS[assert_version],
+        urls = [
+            "https://github.com/bats-core/bats-assert/archive/{}.tar.gz".format(assert_version),
+        ],
+        strip_prefix = "bats-assert-{}".format(assert_version.removeprefix("v")),
+        build_file_content = BATS_LIBRARY_TEMPLATE.format(name = "assert"),
+    )
+
+    http_archive(
+        name = "%s_file" % name,
+        sha256 = BATS_FILE_VERSIONS[file_version],
+        urls = [
+            "https://github.com/bats-core/bats-file/archive/{}.tar.gz".format(file_version),
+        ],
+        strip_prefix = "bats-file-{}".format(file_version.removeprefix("v")),
+        build_file_content = BATS_LIBRARY_TEMPLATE.format(name = "file"),
+    )
+
+    http_archive(
+        name = "%s_toolchains" % name,
+        sha256 = BATS_CORE_VERSIONS[core_version],
+        urls = [
+            "https://github.com/bats-core/bats-core/archive/{}.tar.gz".format(core_version),
+        ],
+        strip_prefix = "bats-core-{}".format(core_version.removeprefix("v")),
+        build_file_content = BATS_CORE_TEMPLATE.format(libraries = [
+            "@%s_support//:support" % name,
+            "@%s_assert//:assert" % name,
+            "@%s_file//:file" % name,
+        ] + libraries),
+    )
+
+    if register:
+        native.register_toolchains("@%s_toolchains//:bats_toolchain" % name)
+
 DEFAULT_COREUTILS_REPOSITORY = "coreutils"
 DEFAULT_COREUTILS_VERSION = _DEFAULT_COREUTILS_VERSION
 
@@ -248,3 +324,4 @@
     register_jq_toolchains()
     register_yq_toolchains()
     register_tar_toolchains()
+    register_bats_toolchains()
diff --git a/lib/tests/bats/BUILD.bazel b/lib/tests/bats/BUILD.bazel
new file mode 100644
index 0000000..32743c1
--- /dev/null
+++ b/lib/tests/bats/BUILD.bazel
@@ -0,0 +1,56 @@
+load("//lib:bats.bzl", "bats_test")
+
+bats_test(
+    name = "basic",
+    size = "small",
+    srcs = [
+        "basic.bats",
+    ],
+)
+
+bats_test(
+    name = "env",
+    size = "small",
+    srcs = [
+        "env.bats",
+    ],
+    env = {
+        "USE_BAZEL_VERSION": "latest",
+    },
+)
+
+bats_test(
+    name = "args",
+    size = "small",
+    srcs = [
+        "basic.bats",
+    ],
+    args = ["--timing"],
+)
+
+bats_test(
+    name = "env_expansion",
+    size = "small",
+    srcs = [
+        "env_expansion.bats",
+    ],
+    data = [
+        "data.bin",
+    ],
+    env = {
+        "DATA_PATH": "$(location :data.bin)",
+    },
+)
+
+bats_test(
+    name = "additional_lib",
+    size = "small",
+    srcs = [
+        "additional_lib.bats",
+    ],
+    target_compatible_with = select({
+        # TODO(thesayyn): incompatible with bzlmod
+        "@aspect_bazel_lib//lib:bzlmod": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+)
diff --git a/lib/tests/bats/additional_lib.bats b/lib/tests/bats/additional_lib.bats
new file mode 100644
index 0000000..0d0034e
--- /dev/null
+++ b/lib/tests/bats/additional_lib.bats
@@ -0,0 +1,8 @@
+bats_load_library 'bats-support'
+bats_load_library 'bats-assert'
+bats_load_library 'bats-custom'
+
+@test 'env' {
+    custom_test_fn
+}
+
diff --git a/lib/tests/bats/basic.bats b/lib/tests/bats/basic.bats
new file mode 100644
index 0000000..ded6a23
--- /dev/null
+++ b/lib/tests/bats/basic.bats
@@ -0,0 +1,8 @@
+bats_load_library 'bats-support'
+bats_load_library 'bats-assert'
+
+@test 'assert_output() check for existence' {
+    run echo 'have'
+    assert_output 'have'
+}
+
diff --git a/lib/tests/bats/bats-custom/BUILD.bazel b/lib/tests/bats/bats-custom/BUILD.bazel
new file mode 100644
index 0000000..be81fe6
--- /dev/null
+++ b/lib/tests/bats/bats-custom/BUILD.bazel
@@ -0,0 +1,10 @@
+load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
+
+copy_to_directory(
+    name = "custom",
+    srcs = [
+        "load.bash",
+    ],
+    out = "bats-custom",
+    visibility = ["//visibility:public"],
+)
diff --git a/lib/tests/bats/bats-custom/load.bash b/lib/tests/bats/bats-custom/load.bash
new file mode 100755
index 0000000..ae1c2af
--- /dev/null
+++ b/lib/tests/bats/bats-custom/load.bash
@@ -0,0 +1,3 @@
+custom_test_fn() {
+	:
+}
diff --git a/lib/tests/bats/data.bin b/lib/tests/bats/data.bin
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/tests/bats/data.bin
diff --git a/lib/tests/bats/env.bats b/lib/tests/bats/env.bats
new file mode 100644
index 0000000..c5f8689
--- /dev/null
+++ b/lib/tests/bats/env.bats
@@ -0,0 +1,8 @@
+bats_load_library 'bats-support'
+bats_load_library 'bats-assert'
+
+@test 'env' {
+    run echo $USE_BAZEL_VERSION
+    assert_output 'latest'
+}
+
diff --git a/lib/tests/bats/env_expansion.bats b/lib/tests/bats/env_expansion.bats
new file mode 100644
index 0000000..b9b55bd
--- /dev/null
+++ b/lib/tests/bats/env_expansion.bats
@@ -0,0 +1,10 @@
+bats_load_library 'bats-support'
+bats_load_library 'bats-assert'
+bats_load_library 'bats-file'
+
+@test 'env expansion' {
+    run echo $DATA_PATH
+    assert_output 'lib/tests/bats/data.bin'
+    assert_file_exists 'lib/tests/bats/data.bin'
+}
+