It scaffolds some test structure in a jekyll-like style and lets pytest know
how to build bitcoin regtest daemons from source:
The philosophy is not to rely on pre-built binaries for bitcoin reference and community implementations (not because we cannot verify them locally or on CI, but because it is a choice to rely on a runtime-specific bitcoin compilation built for the test's purpose).
bitcoindelectrsbtcd(TODO)utreexod(TODO)florestad(TODO)lianad(TODO)
and hands your tests a ready JSON-RPC client — so you write your integration tests, hacking through your preferred bitcoin lib, not plumbing through it.
Warning
This is intended basically for a "krux-ecosystem" integration tests setup (krux, kern, or any related project) without bloating up those repos.
Install:
# prefer pinning to a tag or commit for reproducibility, e.g.
# uv add --dev git+https://github.com/qlrd/bornal.git@<tag-or-commit>
uv add --dev git+https://github.com/qlrd/bornal.gitScaffold
Scaffold an example test, then run it through the pytest plugin:
# We use uv, but you could use poetry or venv
uv run bornal create <feature> --template <template>bornal create <feature> only scaffolds some files into your project.
Run tests
This is the fun part: you skip the main coding setup and just let pytest know
it through their plugin system:
INTEGRATION_TEMP_DIR=<.cache> uv run pytest tests/integration/test_<feature>.pyWhere <.cache> is your pre-built | compiled bitcoin implementation.
build fresh bitcoin nodes
uv run pytest --build-bitcoin latest tests/integration/test_<feature>.pyrun with wallet support
uv run pytest --wallet tests/integration/test_<wallet_feat>.py - Each daemon is a
pytestplugin - each plugin that can be added through a
--build-<name>flag
I.e., pytest --build-bitcoin 30.2 (or latest for the newest release) makes
bitcoind available in the cache and have some additional flags and environment
variables:
- reused unless
--force-build), wipes itsdata/andlogs/ - (unless
--preserve-datais used) - exports
BINARIES_DIR - exports
INTEGRATION_TEMP_DIR - exports
BITCOIN_CORE_PATH --walletto build with wallet support.--nproc Nsets the compile parallelism (the build's-j).
and then runs your tests/integration. If you choose the "compilation-path",
the next time you will not need to use --build-*.
This is for writing tests for your project.
First create a <mytestpath>/conftest.py as in any pytest setup:
import pytest
from bornal.daemon import free_port
from bornal.node import IntegrationTest
# Define a proper setup
class MyTest(IntegrationTest):
"""A simple class to prepare tests"""
def set_test_params(self):
# Alice
self.add_backend("bitcoin-core", p2p_port=free_port())
# Bob
self.add_backend("bitcoin-core", p2p_port=free_port())
def run_test(self):
for backend in self.backends:
self.log.info(f"Running bitcoin (p2p_port={backend.daemon.p2p_port})")
@pytest.fixture(scope="module")
def test_factory(request):
return MyTest
# `integration_test` is an instance of `MyTest`, so you could use any property
# like: `backends[i].daemon`, `backends[i].client`.
@pytest.fixture
def alice(integration_test):
return integration_test.backends[0]
@pytest.fixture
def bob(integration_test):
return integration_test.backends[1]test_factory is a pre-defined fixture. The default one will fail if it isn't
overridden.
integration_test fixture is a customizable instance of the predefined factory
class defined in test_factory fixture.
Nodes stay up until the module's last test. So, it's important to consider the
bitcoin context in a way that the order matters. For example in a file like
<mytestpath>/test_example.py:
from bornal.plugins.bitcoind import UNSPENDABLE_ADDRESS
from bornal.testing import (
assert_block_count,
connect_p2p,
generate_to_address,
sync_blocks
)
def test_000(alice, bob):
assert_block_count(alice, 0)
assert_block_count(bob, 0)
def test_001(alice,bob):
connect_p2p(alice,bob) # Connect P2P already syncs blocks
assert_block_count(bob, 0)
def test_002(alice,bob):
generate_to_address(alice, UNSPENDABLE_ADDRESS, block_amount=1)
assert_block_count(bob, 0)
def test_003(alice,bob):
sync_blocks(alice, bob)
assert_block_count(bob, 1)Daemons are entry-point plugins (group them by bornal.daemons). One plugin
wires three classes:
Compiler(build the binary)Daemon(the implementation daemon on regtest)Client(talk JSON-RPC like) — and declares the pytest build flag it contributes (e.g.,--<bitcoin-impl>→--build-<bitcoin-impl>)
For example:
[project.entry-points."bornal.daemons"]
<bitcoin-impl> = "bornal.plugins.<bitcoin-impl>:CoreCompiler"also you need to register fixtures on src/bornal/fixtures.py so it can be
found by:
[project.entry-points.pytest11]
bornal = "bornal.fixtures"bornal need pytest as dep and not dev-dep.
git clone https://github.com/qlrd/bornal
cd bornaluv sync --group dev
uv run poe hooksuv run poe format-checkuv run poe lintIf you add some plugin, is really important to add some examples on examples/.
They're real-bitcoind integration tests demos.
A previously contribution to the getfloresta/Floresta
test framework taught on how to follow some already coded principles of Bitcoin-Core
test framework and thus I helped to improve it to support both bitcoind, utreexod,
florestad.
While it almost-work (in the sense with random failures in CI), this was fixed by
@joaozinhom using pytest. The idea is that
pytest already deals with threading and plumbing could be made by fixtures.
Afterwards, I learned about the test framework made by wizardsardine/liana that was made before and is pretty similar, with some nice features.
This WIP is a glue of their work as pytest registered plugin plus a
scaffolding and self-tests for custom daemons.
Branch off main and open a PR; reviews scan git diff main...HEAD. Keep the
gates green (uv run poe lint, format-check, test-cov) and use
Conventional Commits — the commit-msg
hook enforces them.