|
| 1 | +# AGENTS.md — Programmatic Integration Tests |
| 2 | + |
| 3 | +This directory contains **programmatic SDK tests** that call `DaprClient` methods directly and assert on return values, gRPC status codes, and SDK types. Unlike the output-based tests in `tests/examples/` (which run example scripts and check stdout), these tests don't depend on print statement formatting. |
| 4 | + |
| 5 | +## How it works |
| 6 | + |
| 7 | +1. `DaprTestEnvironment` (defined in `conftest.py`) manages Dapr sidecar processes |
| 8 | +2. `start_sidecar()` launches `dapr run` with explicit ports, waits for the health check, and returns a connected `DaprClient` |
| 9 | +3. Tests call SDK methods on that client and assert on the response objects |
| 10 | +4. Sidecar stdout is written to temp files (not pipes) to avoid buffer deadlocks |
| 11 | +5. Cleanup terminates sidecars, closes clients, and removes log files |
| 12 | + |
| 13 | +Run locally (requires a running Dapr runtime via `dapr init`): |
| 14 | + |
| 15 | +```bash |
| 16 | +# All integration tests |
| 17 | +tox -e integration |
| 18 | + |
| 19 | +# Single test file |
| 20 | +tox -e integration -- test_state_store.py |
| 21 | + |
| 22 | +# Single test |
| 23 | +tox -e integration -- test_state_store.py -k test_save_and_get |
| 24 | +``` |
| 25 | + |
| 26 | +## Directory structure |
| 27 | + |
| 28 | +``` |
| 29 | +tests/integration/ |
| 30 | +├── conftest.py # DaprTestEnvironment + fixtures (dapr_env, apps_dir, components_dir) |
| 31 | +├── test_*.py # Test files (one per building block) |
| 32 | +├── apps/ # Helper apps started alongside sidecars |
| 33 | +│ ├── invoke_receiver.py # gRPC method handler for invoke tests |
| 34 | +│ └── pubsub_subscriber.py # Subscriber that persists messages to state store |
| 35 | +├── components/ # Dapr component YAMLs loaded by all sidecars |
| 36 | +│ ├── statestore.yaml # state.redis |
| 37 | +│ ├── pubsub.yaml # pubsub.redis |
| 38 | +│ ├── lockstore.yaml # lock.redis |
| 39 | +│ ├── configurationstore.yaml # configuration.redis |
| 40 | +│ └── localsecretstore.yaml # secretstores.local.file |
| 41 | +└── secrets.json # Secrets file for localsecretstore component |
| 42 | +``` |
| 43 | + |
| 44 | +## Fixtures |
| 45 | + |
| 46 | +All fixtures are **module-scoped** — one sidecar per test file. |
| 47 | + |
| 48 | +| Fixture | Type | Description | |
| 49 | +|---------|------|-------------| |
| 50 | +| `dapr_env` | `DaprTestEnvironment` | Manages sidecar lifecycle; call `start_sidecar()` to get a client | |
| 51 | +| `apps_dir` | `Path` | Path to `tests/integration/apps/` | |
| 52 | +| `components_dir` | `Path` | Path to `tests/integration/components/` | |
| 53 | + |
| 54 | +Each test file defines its own module-scoped `client` fixture that calls `dapr_env.start_sidecar(...)`. |
| 55 | + |
| 56 | +## Building blocks covered |
| 57 | + |
| 58 | +| Test file | Building block | SDK methods tested | |
| 59 | +|-----------|---------------|-------------------| |
| 60 | +| `test_state_store.py` | State management | `save_state`, `get_state`, `save_bulk_state`, `get_bulk_state`, `execute_state_transaction`, `delete_state` | |
| 61 | +| `test_invoke.py` | Service invocation | `invoke_method` | |
| 62 | +| `test_pubsub.py` | Pub/sub | `publish_event`, `get_state` (to verify delivery) | |
| 63 | +| `test_secret_store.py` | Secrets | `get_secret`, `get_bulk_secret` | |
| 64 | +| `test_metadata.py` | Metadata | `get_metadata`, `set_metadata` | |
| 65 | +| `test_distributed_lock.py` | Distributed lock | `try_lock`, `unlock`, context manager | |
| 66 | +| `test_configuration.py` | Configuration | `get_configuration`, `subscribe_configuration`, `unsubscribe_configuration` | |
| 67 | + |
| 68 | +## Port allocation |
| 69 | + |
| 70 | +All sidecars default to gRPC port 50001 and HTTP port 3500. Since fixtures are module-scoped and tests run sequentially, only one sidecar is active at a time. If parallel execution is needed in the future, sidecars will need dynamic port allocation. |
| 71 | + |
| 72 | +## Helper apps |
| 73 | + |
| 74 | +Some building blocks (invoke, pubsub) require an app process running alongside the sidecar: |
| 75 | + |
| 76 | +- **`invoke_receiver.py`** — A `dapr.ext.grpc.App` that handles `my-method` and returns `INVOKE_RECEIVED`. |
| 77 | +- **`pubsub_subscriber.py`** — Subscribes to `TOPIC_A` and persists received messages to the state store. This lets tests verify message delivery by reading state rather than parsing stdout. |
| 78 | + |
| 79 | +## Adding a new test |
| 80 | + |
| 81 | +1. Create `test_<building_block>.py` |
| 82 | +2. Add a module-scoped `client` fixture that calls `dapr_env.start_sidecar(app_id='test-<name>')` |
| 83 | +3. If the building block needs a new Dapr component, add a YAML to `components/` |
| 84 | +4. If the building block needs a running app, add it to `apps/` and pass `app_cmd` / `app_port` to `start_sidecar()` |
| 85 | +5. Use unique keys/resource IDs per test to avoid interference (the sidecar is shared within a module) |
| 86 | +6. Assert on SDK return types and gRPC status codes, not on string output |
| 87 | + |
| 88 | +## Gotchas |
| 89 | + |
| 90 | +- **Requires `dapr init`** — the tests assume a local Dapr runtime with Redis (`dapr_redis` container on `localhost:6379`), which `dapr init` sets up automatically. |
| 91 | +- **Configuration tests seed Redis directly** via `docker exec dapr_redis redis-cli`. |
| 92 | +- **Lock and configuration APIs are alpha** and emit `UserWarning` on every call. Tests suppress these with `pytestmark = pytest.mark.filterwarnings('ignore::UserWarning')`. |
| 93 | +- **`localsecretstore.yaml` uses a relative path** (`secrets.json`) resolved against `cwd=INTEGRATION_DIR`. |
| 94 | +- **Dapr may normalize response fields** — e.g., `content_type` may lose charset parameters when proxied through gRPC. Assert on the media type prefix, not the full string. |
0 commit comments