Skip to main content

Mock Binary Ninja API for testing Binary Ninja plugins without requiring a license

Project description

binja-test-mocks

CI PyPI version Python versions

Mock Binary Ninja API for testing Binary Ninja plugins without requiring a Binary Ninja license.

Overview

binja-test-mocks provides a comprehensive set of mock objects and utilities that allow you to:

  • Unit test Binary Ninja plugins without a Binary Ninja installation
  • Run type checking with mypy/pyright using accurate type stubs
  • Develop and test plugins in CI/CD environments

Installation

pip install binja-test-mocks

With uv:

uv add --dev binja-test-mocks pytest

For development:

pip install -e /path/to/binja-test-mocks

Quick Start

Recommended pytest setup (mocks only in tests/CI)

# tests/conftest.py
#
# Import binja-test-mocks *before* importing anything that does `import binaryninja`.
# This keeps mocks scoped to unit tests/CI and avoids impacting real Binary Ninja.
from __future__ import annotations

import importlib.util
import os

def _running_inside_binary_ninja() -> bool:
    try:
        return importlib.util.find_spec("binaryninjaui") is not None
    except (ValueError, ImportError):
        return False

if not _running_inside_binary_ninja():
    os.environ.setdefault("FORCE_BINJA_MOCK", "1")

    # Installs a stubbed `binaryninja` module into `sys.modules`.
    from binja_test_mocks import binja_api  # noqa: F401

    # Optional but common: configure architecture-specific IL size suffixes.
    from binja_test_mocks import mock_llil

    mock_llil.set_size_lookup(
        {1: ".b", 2: ".w", 4: ".d", 8: ".q", 16: ".o"},
        {"b": 1, "w": 2, "d": 4, "q": 8, "o": 16},
    )

Example: lift bytes to LLIL

from binaryninja import lowlevelil
from binja_test_mocks.mock_llil import MockLabel, MockLLIL, mllil

from your_plugin.arch import MyArchitecture

def lift_all(data: bytes, *, start_addr: int = 0) -> list[MockLLIL]:
    arch = MyArchitecture()
    il = lowlevelil.LowLevelILFunction(arch)

    offset = 0
    while offset < len(data):
        il.current_address = start_addr + offset  # type: ignore[attr-defined]
        length = arch.get_instruction_low_level_il(data[offset:], start_addr + offset, il)
        assert length is not None and length > 0
        offset += length

    # Mock LLIL emits LABEL pseudo-nodes for control-flow; ignore them.
    return [node for node in il if not isinstance(node, MockLabel)]

def test_instruction_lifting() -> None:
    assert lift_all(b"\x90") == [mllil("NOP")]

Safe Integration Guide (Binary Ninja plugins)

Keep mocks scoped to tests/CI

  • Put the binja_test_mocks.binja_api import in tests/conftest.py (not in your plugin package).
  • Set FORCE_BINJA_MOCK=1 only for test runs (CI job env, pytest, etc.).
  • Keep binja-test-mocks in dev/test dependencies (don’t require it at runtime in Binary Ninja).

binja_test_mocks.binja_api is defensive: even if FORCE_BINJA_MOCK=1 is set globally, it will refuse to install mocks when it detects it’s running inside the Binary Ninja application process (unless you explicitly set ALLOW_BINJA_MOCK_IN_BINARY_NINJA=1).

Avoid registration side effects during tests

If your plugin registers architectures/commands at import time, tests that import your package may accidentally run that registration code. A robust pattern is:

  • your_plugin/_bn_plugin.py: define register() (calls Architecture.register(), PluginCommand.register_*(), etc.)
  • your_plugin/__init__.py: call register() only when running inside Binary Ninja (and not under FORCE_BINJA_MOCK)

This is the same approach used by mblsha/binaryninja-m68k (see mblsha/binaryninja-m68k#1).

Write tests against bytes (disasm + LLIL)

  • Disassembly: arch.get_instruction_text(data, addr) → join token .text → compare to expected string.
  • LLIL: arch.get_instruction_low_level_il(...) into a LowLevelILFunction → compare the resulting MockLLIL tree.
  • Control flow: the mock IL may include MockLabel nodes; filter or assert them as needed.

If your plugin needs more binaryninja.* surface than is currently mocked, prefer adding it here (via PR) instead of copy/pasting ad-hoc stubs into each plugin repository.

Components

Mock Modules

  • binja_api.py: Core mock loader that intercepts Binary Ninja imports
  • mock_llil.py: Mock Low Level IL classes and operations
  • mock_binaryview.py: Mock BinaryView for testing file format plugins
  • mock_analysis.py: Mock analysis information (branches, calls, etc.)
  • tokens.py: Token generation utilities for disassembly
  • coding.py: Binary encoding/decoding helpers
  • eval_llil.py: LLIL expression evaluator for testing

Type Stubs

Complete type stubs for Binary Ninja API in stubs/binaryninja/:

  • architecture.pyi
  • binaryview.pyi
  • lowlevelil.pyi
  • enums.pyi
  • types.pyi
  • function.pyi
  • log.pyi
  • interaction.pyi

Integration Examples

Plugin entrypoint pattern (safe with tests)

# your_plugin/__init__.py
from __future__ import annotations

import importlib.util
import os
import sys
from pathlib import Path

_plugin_dir = Path(__file__).resolve().parent
if str(_plugin_dir) not in sys.path:
    sys.path.insert(0, str(_plugin_dir))

def _running_inside_binary_ninja() -> bool:
    try:
        return importlib.util.find_spec("binaryninjaui") is not None
    except (ValueError, ImportError):
        return False

_force_mock = os.environ.get("FORCE_BINJA_MOCK", "").lower() in ("1", "true", "yes")
_skip_registration = _force_mock and not _running_inside_binary_ninja()

if not _skip_registration:
    # Keep registration in a separate module to avoid side effects in unit tests.
    from ._bn_plugin import register

    register(plugin_dir=_plugin_dir)

Type Checking Configuration

mypy.ini

[mypy]
mypy_path = /path/to/binja-test-mocks/src/binja_test_mocks/stubs
plugins = mypy_binja_plugin

[mypy-binaryninja.*]
ignore_missing_imports = False

pyrightconfig.json

{
  "extraPaths": [
    "/path/to/binja-test-mocks/src/binja_test_mocks/stubs"
  ],
  "typeCheckingMode": "strict"
}

Running Tests

# Typical (with `tests/conftest.py` setting `FORCE_BINJA_MOCK`)
pytest

# With uv
uv run pytest

# Belt-and-suspenders: force mocks even if you don't have a conftest
FORCE_BINJA_MOCK=1 uv run pytest

# Bundled runner (same as running pytest under the hood)
binja-test-runner

Advanced Usage

Custom Mock Behavior

from binja_test_mocks.mock_llil import MockLowLevelILFunction

class CustomMockIL(MockLowLevelILFunction):
    def __init__(self):
        super().__init__()
        self.custom_data = []
    
    def append(self, expr):
        self.custom_data.append(expr)
        return super().append(expr)

Testing Binary Views

from binja_test_mocks.mock_binaryview import MockBinaryView

def test_binary_view_parsing():
    data = b"\x4d\x5a\x90\x00"  # PE header
    bv = MockBinaryView(data)
    
    # Your binary view implementation
    my_view = MyBinaryView(bv)
    assert my_view.init()

Migration from binja_helpers

If you're migrating from the old binja_helpers:

  1. Update imports:

    # Old
    from binja_helpers import binja_api
    
    # New
    from binja_test_mocks import binja_api
    
  2. Update path additions if needed:

    # Old
    sys.path.insert(0, str(plugin_dir / "binja_helpers_tmp"))
    
    # New - not needed if installed via pip
    

Contributing

Contributions are welcome! Please ensure:

  • All tests pass with pytest
  • Type checking passes with mypy and pyright
  • Code is formatted with ruff

License

MIT License - see LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

binja_test_mocks-0.1.11.tar.gz (26.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

binja_test_mocks-0.1.11-py3-none-any.whl (30.3 kB view details)

Uploaded Python 3

File details

Details for the file binja_test_mocks-0.1.11.tar.gz.

File metadata

  • Download URL: binja_test_mocks-0.1.11.tar.gz
  • Upload date:
  • Size: 26.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for binja_test_mocks-0.1.11.tar.gz
Algorithm Hash digest
SHA256 906555a36f11e22f995e5056e52e18f7c1be499cb99f1cf554985aea326946e4
MD5 b9b262468fd06a3f848da37da6498a0e
BLAKE2b-256 8ac82862ea8b82e9ac77b3ea35b59ce5b006d955356a25809933803c369d19b8

See more details on using hashes here.

Provenance

The following attestation bundles were made for binja_test_mocks-0.1.11.tar.gz:

Publisher: publish.yml on mblsha/binja-test-mocks

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file binja_test_mocks-0.1.11-py3-none-any.whl.

File metadata

File hashes

Hashes for binja_test_mocks-0.1.11-py3-none-any.whl
Algorithm Hash digest
SHA256 4e83a3ac89cf0cb085ec8bbc54cdc2d7ceb7994feb04fd0b807aa2365d54aa51
MD5 48fcc32374335c5892072eef6aaf8eef
BLAKE2b-256 6a85169672305992e6e78e2af4d3d0f5341f0442f3d7ceefdb7905a9920e1968

See more details on using hashes here.

Provenance

The following attestation bundles were made for binja_test_mocks-0.1.11-py3-none-any.whl:

Publisher: publish.yml on mblsha/binja-test-mocks

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page