BTEng includes mock nodes and a small test harness that let you unit-test behavior trees without real hardware, network calls, or side effects. The goal is to test tree structure and control logic in isolation.
BehaviorTreeTest runs a tree with configurable expectations and returns a structured
result:
from bteng import (
BehaviorTreeTest,
MockActionNode,
MockConditionNode,
NodeStatus,
SequenceNode,
Tree,
TreeMetadata,
)
condition = MockConditionNode("Ready")
condition.set_result(True) # returns SUCCESS every tick
action = MockActionNode("Work")
action.set_ticks_to_complete(2) # returns RUNNING×2, then SUCCESS
root = SequenceNode("root", children=[condition, action])
tree = Tree(TreeMetadata(id="first_test"), root)
result = (
BehaviorTreeTest(tree)
.expect_final_status(NodeStatus.SUCCESS)
.set_max_ticks(10)
.run()
)
print(result.passed)
print(result.tick_count)
Expected output:
True
3
If expectations are not met (wrong final status, exceeded max ticks), result.passed
is False and result.error_message contains a description.
assert result, result.error_message # short-circuit assertion
Configures an action to return a preset sequence of statuses:
from bteng import MockActionNode, NodeStatus
mock = MockActionNode("Navigate")
# Option 1: run for N ticks then succeed
mock.set_ticks_to_complete(3) # RUNNING×3, then SUCCESS
# Option 2: always return the same status
mock.set_result(NodeStatus.FAILURE)
# Option 3: explicit status sequence
mock.set_results([
NodeStatus.RUNNING,
NodeStatus.RUNNING,
NodeStatus.SUCCESS,
])
Query state after the run:
mock.tick_count # how many times execute_tick() was called
mock.was_halted # True if halt() was called at least once
mock.halt_count # number of times halted
halt_count > 0 is particularly useful for testing reactive interrupts — verify that
a running action was halted when a guard condition failed.
Returns a preset boolean result:
from bteng import MockConditionNode
cond = MockConditionNode("IsReady")
cond.set_result(True) # returns SUCCESS
cond.set_result(False) # returns FAILURE
Simulates a long-running action with configurable tick duration:
from bteng import SimulatedActionNode
sim = SimulatedActionNode("LongTask", ticks_to_run=5)
# RUNNING×5, then SUCCESS
Useful when you want to test a Retry or Timeout decorator without writing a custom mock.
When a node reads or writes the blackboard, construct the tree with a real blackboard and verify the blackboard state after the run:
from bteng import (
Blackboard, BehaviorTreeTest, ConditionNode, MockActionNode,
NodeConfig, NodeStatus, SequenceNode, Tree, TreeMetadata,
)
bb = Blackboard.create("test_bb")
bb.set("ready", True)
class IsReady(ConditionNode):
def tick(self):
return NodeStatus.SUCCESS if self.blackboard.get("ready") else NodeStatus.FAILURE
cfg = NodeConfig(blackboard=bb)
mock = MockActionNode("Work")
mock.set_ticks_to_complete(1)
root = SequenceNode("root", children=[IsReady("check", cfg), mock])
tree = Tree(TreeMetadata(id="bb_test"), root)
result = (
BehaviorTreeTest(tree)
.expect_final_status(NodeStatus.SUCCESS)
.run()
)
assert result, result.error_message
Blackboard.reset("test_bb") # clean up between tests
To test that a reactive node halts an action when a guard condition changes, write a custom node that modifies the blackboard mid-run:
from bteng import ActionNode, Blackboard, NodeStatus
class BreakPath(ActionNode):
"""Sets path_clear=False on first tick, then returns RUNNING forever."""
def tick(self):
self.blackboard.set("path_clear", False)
return NodeStatus.RUNNING
Wire it into a ReactiveSequenceNode alongside a MockActionNode and verify that
mock.was_halted is True after the test.
No special plugin is needed. Standard pytest patterns work:
import pytest
from bteng import Blackboard
@pytest.fixture(autouse=True)
def clean_blackboard():
"""Reset shared blackboard state between every test."""
yield
Blackboard.reset("robot")
def test_navigate_succeeds():
from bteng import (
BehaviorTreeTest, MockActionNode, MockConditionNode,
NodeStatus, SequenceNode, Tree, TreeMetadata,
)
cond = MockConditionNode("BatteryOK")
cond.set_result(True)
action = MockActionNode("Navigate")
action.set_ticks_to_complete(2)
root = SequenceNode("root", children=[cond, action])
tree = Tree(TreeMetadata(id="navigate_test"), root)
result = (
BehaviorTreeTest(tree)
.expect_final_status(NodeStatus.SUCCESS)
.set_max_ticks(10)
.run()
)
assert result, result.error_message
def test_navigate_fails_when_battery_low():
from bteng import (
BehaviorTreeTest, MockActionNode, MockConditionNode,
NodeStatus, SequenceNode, Tree, TreeMetadata,
)
cond = MockConditionNode("BatteryOK")
cond.set_result(False) # battery is dead — sequence should fail immediately
action = MockActionNode("Navigate")
action.set_ticks_to_complete(5)
root = SequenceNode("root", children=[cond, action])
tree = Tree(TreeMetadata(id="battery_test"), root)
result = (
BehaviorTreeTest(tree)
.expect_final_status(NodeStatus.FAILURE)
.set_max_ticks(10)
.run()
)
assert result, result.error_message
assert action.tick_count == 0 # action was never reached
| Mistake | Fix |
|---|---|
Forgetting Blackboard.reset() in teardown |
State leaks between tests; use a pytest.fixture to reset |
| Testing implementation details instead of behavior | Assert on final status and blackboard state, not on internal node fields |
Using tick_until_result in tests without max_ticks |
A tree stuck in RUNNING will run forever; always set a limit |
Not checking was_halted for reactive interrupt tests |
The action may finish naturally rather than being interrupted — verify with halt_count |
Next: the Practical Recipes section shows full end-to-end patterns for common behavior-tree problems.