Skip to content

Testing Guide

Stephen Cox edited this page Sep 12, 2026 · 1 revision

qbt-rules has a real pytest suite covering every module — this page is a map of what's there and how to run it, not a tutorial on pytest itself.

Test Layout

tests/
├── unit/            # One file per module, isolated from external services
│   ├── test_api.py
│   ├── test_arguments.py
│   ├── test_cli.py
│   ├── test_config.py
│   ├── test_engine/
│   ├── test_errors.py
│   ├── test_logging.py
│   ├── test_queue_manager.py
│   ├── test_redis_queue.py
│   ├── test_resolver.py
│   ├── test_resolver_edge_cases.py
│   ├── test_server.py
│   ├── test_sqlite_queue.py
│   ├── test_utils.py
│   └── test_worker.py
└── integration/      # Cross-module, end-to-end scenarios
    ├── test_advanced_example.py
    ├── test_full_stack.py
    ├── test_resolver_e2e.py
    ├── test_resolver_integration.py
    └── test_rule_execution.py

test_engine/ being a directory rather than a single file reflects how much surface engine.py has (ConditionEvaluator, ActionExecutor, RulesEngine — see Architecture Internals).

Running Tests

# Everything
pytest

# One file
pytest tests/unit/test_resolver.py

# Only unit tests, skip integration
pytest tests/unit

# Skip anything marked slow or requiring Redis
pytest -m "not slow and not redis"

Three custom markers are registered in pyproject.toml (--strict-markers means an unregistered marker is a hard error, not a silent no-op):

Marker Meaning
slow Long-running test; deselect with -m "not slow" for a fast local loop
integration Exercises more than one module together
redis Requires a live Redis instance; deselect with -m "not redis" if you don't have one running

Coverage

Coverage is on by default (--cov=qbt_rules --cov-report=term-missing --cov-report=html is baked into pyproject.toml's addopts, so plain pytest always reports it):

pytest
# summary prints to the terminal; full HTML report lands in htmlcov/
open htmlcov/index.html

Writing Tests for New Code

If you're following Extending the Engine to add an operator or action:

  • A new operator in ConditionEvaluator._apply_operator belongs in tests/unit/test_engine/ — assert it directly against both scalar and collection field values (collections exercise the ANY/ALL-match branching described in Conditions).
  • A new action in ActionExecutor belongs in the same directory — assert both the "changes something" path and the idempotent skip path (if applicable) against a mocked api.py client, the same pattern the built-in actions use.
  • A new queue backend under queue_backends/ should implement the same test contract test_sqlite_queue.py/test_redis_queue.py already exercise against queue_manager.py's abstract interface, so it's verified to be a drop-in replacement.
  • Anything touching the resolver (refs/$ref/${vars.x}) has both a focused unit suite (test_resolver.py, test_resolver_edge_cases.py) and an end-to-end integration suite (test_resolver_e2e.py, test_resolver_integration.py) — new resolver behavior should get coverage in both, since edge cases (circular refs, type mismatches) are easy to get right in isolation and wrong once a real rule file is involved.

Before opening a pull request, run the full suite plus a real config validation pass — see Contributing for the complete pre-PR checklist.

🎯 qbt-rules

GitHub • Releases


📚 Getting Started

📖 Core Concepts

🔒 Security

🔧 Configuration

🆘 Help & Support

🧑‍💻 For Developers


🔑 Key Features

  • 🚀 Client-server architecture (HTTP API + worker)
  • 📦 Persistent job queue (SQLite or Redis)
  • ♻️ Reusable references ($ref, ${vars.x})
  • 🐳 Docker-first deployment

🔗 Quick Links

Clone this wiki locally