---
description: BDD/Behave testing conventions for ArchiPy feature files and step implementations
globs: features/**/*.py,features/**/*.feature
alwaysApply: false
---

# Testing with BDD (Behave)

ArchiPy uses the **Behave** BDD framework (v1.3.3). Tests are written as Gherkin scenarios.

## Directory Structure

```
features/
├── *.feature                    # Gherkin scenario files
├── environment.py               # Global setup/teardown hooks
├── scenario_context.py          # Per-scenario state container
├── test_helpers.py              # Shared async/sync utilities
└── steps/
    └── *.py                     # Step implementations (Given/When/Then)
```

## Running Tests

```bash
make behave
uv run --extra behave behave features/file_name.feature
uv run --extra behave behave features/file_name.feature:42
```

## Writing Feature Files

- Keep `.feature` files **declarative** — describe behavior, not implementation.
- Use `@tags` to categorize scenarios for selective runs.

```gherkin
# ✅ GOOD
@redis @cache
Feature: Cache adapter
  Scenario: Store and retrieve a value
    Given a running Redis instance
    When I store "hello" under key "greeting"
    Then I can retrieve "hello" from key "greeting"
```

## Writing Step Files

- Step functions are exempt from: `ANN001`, `ANN201`, `ARG001`, `PLR0913`, `F811`.
- Step redefinition (`F811`) is allowed — Behave reuses steps across features.
- Use `get_current_scenario_context(context)` to share state between steps.

## Async Steps

Behave 1.3.3 supports `async def` step functions **natively** — no `asyncio.run()` or `@async_run_until_complete` wrapper needed. Always declare async steps with `async def` and use `await` directly.

```python
# ❌ BAD — never use asyncio.run() inside a step
@when("something async happens")
def step_impl(context):
    result = asyncio.run(some_async_call())

# ✅ GOOD — declare the step as async def and await directly
@when("something async happens")
async def step_impl(context):
    result = await some_async_call()
```

Sync operations work fine inside `async def` steps, so steps with mixed sync/async branches (e.g. if/elif over frameworks) should be converted to `async def` entirely.

The only place `asyncio.run()` is acceptable is in **non-step** callbacks (e.g. `scenario_context.py` cleanup methods) that may be called from outside an event loop.

## Parallel Execution

Behave is configured to run with **8 parallel workers** (multiprocessing). Steps must not share mutable global state across scenarios.

## Linting Exclusions

`features/` and `scripts/` are excluded from Ruff linting. Do not add type annotations to step functions.
