# speclike

**A helper library for pytest designed to make test code clearer and more structured.**

The following is reproduced from `__init__.py`.

# speclike

`speclike` is a **pytest helper library** designed to define tests in a more structured and expressive way.  
It provides a declarative approach for building tests from two complementary perspectives:

- **Individual test bodies**, written as ordinary methods.
- **Externally defined dispatchers and actors**, representing scenario-driven or behavior-based tests.

The framework automatically generates executable pytest test functions (`test_...`) from decorated functions and classes.

---

## 🧩 Core Concepts

### 1. `Spec` and `ExSpec` Classes
- **`Spec`** — the main base class for declarative test specifications.  
  It manages auto-generated tests and delegates execution through `dispatch()` or `dispatch_async()`.
- **`ExSpec`** — groups externally defined dispatchers (functions that control test flow outside of the class).

Both are implemented using metaclasses (`_SpecMeta`, `_ExSpecMeta`) that synthesize pytest-compatible test functions during class creation.

---

### 2. `Case` and `Ex` Decorators
- **`Case`** — marks individual test bodies or actor functions (`def _(...):`) within a `Spec` class.  
  It can attach pytest marks, parametrize data, or skip tests dynamically.
- **`Ex`** — marks dispatcher functions used in `ExSpec` or top-level definitions.  
  Dispatchers define parameter structure using `PRM`, and connect to actors via `@case.ex(dispatcher)`.

---

### 3. `PRM` (Parameter Prefix Rules)
Defines how test parameters behave and interact between dispatcher and actor.

| Prefix | Kind | Behavior |
|---------|------|-----------|
| `_` | AO (Actor-Only) | Created by dispatcher, passed to actor (not parametrized) |
| *(none)* | AP (Actor-Parametrized) | Parametrized and passed to actor |
| `__` | PO (Param-Only) | Parametrized but **not** passed to actor (used for assertions) |

Parameter ordering must follow `AO → AP → PO`.

`PRM` validates actor signatures, generates pytest parametrization, and bridges runtime values through `_ParamsBridge`.

---

### 4. `Dispatcher` and `Actor`
- **Dispatcher**: a function decorated with `@ex` that defines test input combinations using `PRM`.
- **Actor**: a function named `_` decorated with `@case.ex(dispatcher)` that performs the actual behavior under test.
- The library automatically links each actor to its dispatcher and generates a `test_<dispatcher>` method that executes the pair.

---

### 5. Test Generation Workflow
1. The metaclass scans for decorated functions (`TargetKind`).
2. Each test body or dispatcher/actor pair is converted into a pytest-visible `test_...` function.
3. Signatures, parametrization, and pytest marks are copied to preserve readability and IDE support.
4. For external specs (`ExSpec`), tests are created dynamically based on defined dispatchers.

---

### 6. Highlights
- Strong signature validation for actors against their `PRM` definitions.
- Automatic propagation of `pytest.mark.parametrize` and other pytest marks.
- Source location (`co_firstlineno`) is preserved for accurate traceback references.
- Supports both sync and async test execution paths.

---

### Example

```python
from speclike import Spec, PRM

# Example domain object
class Context:
    def compute(self, x: int) -> int:
        return x * 10

# Get decorators
case, ex = Spec.get_decorators()

# Dispatcher (external). Parameters are NOT taken as direct function args.
# Access AP/PO values via the bridge `p`, and call the actor via `p.act(...)`.
@ex.follows(
    [(1, 10), (2, 20), (3, 30)],  # (value, __expected)
    ids=["x1", "x2", "x3"]
)
def check(p = PRM(_ctx=Context, value=int, __expected=int)):
    ctx = Context()                             # AO: create here
    result = p.act(_ctx=ctx, value=p.value)     # call actor with AO/AP
    assert result == p.__expected               # PO: only used in dispatcher

# Spec class with actor method named "_"
class TestCompute(Spec):
    @case.ex(check)
    def _(self, _ctx: Context, value: int) -> int:
        # Actor receives AO/AP only, in the declared order.
        return _ctx.compute(value)
````

At runtime, this generates:

* `test_check` — a parametrized pytest function executing the dispatcher.
* `act_for_check` — an internal bound actor function used by the dispatcher.


Installation

pip
```bash
pip install speclike
```

github
```bash
pip install git+https://github.com/minoru-jp/speclike.git
```

---

## Status

This project is in **very early development (alpha stage)**.  
APIs and behavior may change without notice.

---

## License

MIT License © 2025 minoru_jp