bteng

Testing Your First Tree

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

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

Mock nodes

MockActionNode

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.

MockConditionNode

Returns a preset boolean result:

from bteng import MockConditionNode

cond = MockConditionNode("IsReady")
cond.set_result(True)    # returns SUCCESS
cond.set_result(False)   # returns FAILURE

SimulatedActionNode

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.


Testing with a real blackboard

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

Testing reactive behavior

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.


Integration with pytest

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

Common testing mistakes

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.