Skip to content

feat(template): add real-broker worker integration tests - #49

Merged
hasansezertasan merged 2 commits into
mainfrom
feat/worker-broker-testing
Jul 5, 2026
Merged

feat(template): add real-broker worker integration tests#49
hasansezertasan merged 2 commits into
mainfrom
feat/worker-broker-testing

Conversation

@hasansezertasan

@hasansezertasan hasansezertasan commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

Extracts the testing patterns from litestar-faststream and litestar-events#9 into the worker component, and fixes the systemic coverage-gate failures the work surfaced. Design of record: docs/adr/008-worker-broker-testing-strategy.md.

Worker testing

  • build_broker(url=None) factory replaces the module-level broker singleton: registers the subscriber/publisher and resolves the connection at call time, so tests can point a fresh, self-contained broker at a throwaway instance without import-order/caching pitfalls. The module still exposes broker = build_broker() for the entry point.
  • Unit tests rewritten to the collector-subscriber pattern — assert on the response delivered to a collector, not internal handler/publisher wiring. New tests pin the factory contract with a constructor spy (explicit URL is actually forwarded; env-var fallback is read — previously a dropped url parameter would only surface in the Docker-only CI job) and the malformed-request path (validation error, no response published).
  • New integration test (tests/worker/test_integration.py, marked integration, deselected from the default suite) starts a real broker via testcontainers and round-trips a message through the actual driver, asserting the full response payload. Hardened against the real world: the request is (re)published inside the poll loop because kafka/redis/nats subscriptions are at-most-once from a cold consumer (a single up-front publish was a built-in race); broker.start() is bounded by asyncio.wait_for and each test by pytest-timeout, so a wedged container fails with a traceback instead of hanging until the CI job kill; kafka gets a larger poll window for cold-runner consumer-group joins.
  • On-demand tox -e integration env (Docker required) and a conditional, Ubuntu-only worker-integration CI job that gates the check aggregation job. The env demotes DeprecationWarnings broadly (not per-module — CLI -W module matching is exact and drivers attribute warnings via stacklevel to arbitrary caller modules, verified empirically with the redis driver); the default suite stays strict.

Systemic coverage fixes (fail_under = 99)

  • Per-site pragmas instead of blanket exclude_lines. An earlier iteration excluded def main(/async def run_server/except PackageNotFoundError via [tool.coverage.report] exclude_lines; that was rejected during review — a regex matching a def line excludes the entire function body, which silently un-measured the tested GUI/TUI entrypoints and the web 503 handlers. Now only the genuinely blocking entrypoints carry # pragma: no cover (web/MCP/worker main(), MCP run_server, the __main__ dispatchers, the worker's module-level metadata fallback — which now logs).
  • Reachable error handling is tested, not excluded: web /version + /info 503 responses (FastAPI and Litestar), the CLI metadata-failure exit-code-1 contract, the GUI/TUI "Version: unknown" degradation, the MCP error-text response, and the GUI/TUI main() success paths (a coverage hole the blanket pattern had been hiding). Patch targets go through importlib.import_module because the cli/web/worker __init__ re-export app, shadowing the submodule on any attribute-based import.
  • Coverage omit globs are filename-anchored (*/_version.py) — the previous src/** pattern silently missed tox's site-packages installs and reported _version.py at 0% for every generated project. The integration omit is additionally directory-anchored (*/worker/test_integration.py) so a future test_integration.py in another component stays measured.
  • [tool.coverage.paths] now remaps */site-packages/<pkg> back to src/<pkg>, so multi-env coverage combine unifies per-interpreter data instead of counting version-gated lines as missed (base project fell to ~92% across five envs without this).

Validation

  • All four brokers (kafka/nats/rabbitmq/redis): unit suites pass; real-container integration round-trips pass (Docker, all four brokers).
  • All-components project (CLI+web+GUI+TUI+MCP+worker, rabbitmq, 20-char repo name to exercise line-length rendering): 100% coverage with the gate green and the full style suite (ruff + mypy/basedpyright/ty/pyrefly/zuban + vulture + taplo + …) passing.
  • Litestar-web and base (all-disabled) projects: 100% coverage, tests green.

No linked issue — template maintenance extracted from external-repo analysis (label: no-issue).

Extract the testing patterns proven in litestar-faststream and
litestar-events#9 into the worker component, and fix the systemic
coverage-gate failures the work surfaced (ADR-008).

Worker testing:
- Replace the module-level broker singleton with a build_broker(url=None)
  factory that registers the subscriber/publisher and resolves the
  connection at call time, giving tests an injectable seam.
- Rewrite unit tests to the collector-subscriber pattern (assert on
  observable behavior, not internal wiring).
- Add a testcontainers-backed integration test (marked `integration`,
  deselected by default) that round-trips a message through the real
  broker driver, plus an on-demand `tox -e integration` env and a
  conditional Ubuntu-only `worker-integration` CI job gating `check`.

Coverage gate (fail_under = 99), systemic fixes:
- Exclude untestable process/UI/server entrypoints and the
  PackageNotFoundError fallback via exclude_lines; pragma the CLI
  launcher subcommands, GUI/TUI display helpers, worker lifecycle
  hooks, and the c-extension import fallback.
- Anchor coverage omit globs on filenames (*/_version.py) so they also
  match tox's site-packages installs (src/** silently missed them).
- Remap [tool.coverage.paths] to */site-packages/<pkg> so multi-env
  `coverage combine` unifies per-interpreter data instead of counting
  version-gated lines as missed.

Validated for all four brokers (kafka/nats/rabbitmq/redis): full style
suite, multi-env tox with 100% coverage, and real-container integration
round-trips. Documented in docs/adr/008-worker-broker-testing-strategy.md,
CLAUDE.md, and the generated README.
@sourcery-ai

sourcery-ai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a broker factory-based worker design to enable real-broker integration testing with testcontainers, adds a dedicated tox env and CI job for integration tests, and fixes systemic coverage configuration issues (paths, omit patterns, and untestable entrypoints) across the template while updating worker/CLI/GUI/TUI code and docs accordingly.

Sequence diagram for the worker broker factory and integration test

sequenceDiagram
  actor Tester
  participant IntegrationTest
  participant TestcontainersBroker
  participant build_broker
  participant Broker
  participant handle_version_request
  participant Publisher

  Tester->>IntegrationTest: run pytest -m integration
  IntegrationTest->>TestcontainersBroker: start()  # from testcontainers
  TestcontainersBroker-->>IntegrationTest: return broker_url

  IntegrationTest->>build_broker: build_broker(url)
  build_broker-->>Broker: return configured broker

  build_broker->>Broker: broker.publisher("version-responses")
  build_broker->>Broker: broker.subscriber("version-requests")
  Broker-->>handle_version_request: register handler via decorator

  IntegrationTest->>Broker: [send VersionRequest]
  Broker-->>handle_version_request: invoke with VersionRequest
  handle_version_request->>Publisher: publisher.publish(VersionResponse)
  Publisher-->>IntegrationTest: [collector receives VersionResponse]
  IntegrationTest-->>Tester: assert on collected VersionResponse
Loading

File-Level Changes

Change Details Files
Refactor worker broker setup to a factory function to support testability and integration tests.
  • Replace module-level broker singleton with build_broker(url=None) that configures broker URL, publisher, and subscriber at call time
  • Instantiate a default broker via build_broker() and wire FastStream(app) to it
  • Adjust handler to publish through a closure-scoped publisher, handling NatsBroker’s None return value and silencing unused-closure warnings by referencing the handler
  • Mark worker lifecycle hooks on_startup/on_shutdown as no cover for coverage
template/src/{{github_repo_name}}/worker/app.py.jinja
Rework worker unit tests to use the broker factory and observable collector pattern.
  • Import build_broker instead of internal handler/publisher symbols
  • Add test to verify build_broker accepts an explicit URL and returns a fresh broker instance
  • Rewrite version request test to create a fresh broker via build_broker, attach a collector subscriber, and assert on collected response data using FastStream’s in-memory Test
template/tests/worker/test_app.py.jinja
Add real-broker integration testing via testcontainers and wire it into tox/pytest/CI.
  • Add testcontainers[] to test dependency group when include_worker is enabled
  • Introduce pytest integration marker and default addopts that deselect integration tests; register warning filter for testcontainers’ deprecated decorator
  • Create template worker integration test that starts the chosen broker in a container, builds a broker against it via build_broker(url), publishes a request, and polls for a response
  • Define a dedicated tox env integration that runs pytest -m integration tests/worker with relaxed DeprecationWarning handling
  • Document integration test usage in CLAUDE.md and README, including uv/ tox invocation and CI behavior
  • Add Ubuntu-only worker-integration CI job that runs tox run -e integration and gates the check aggregation job
template/pyproject.toml.jinja
template/tests/worker/test_integration.py.jinja
template/.github/workflows/ci.yml.jinja
CLAUDE.md
template/README.md.jinja
Tighten and generalize coverage configuration to support multi-env tox runs and untestable entrypoints.
  • Change coverage.run omit glob from src/**/_version.py to */_version.py and also omit */test_integration.py when include_worker, for both run and report sections
  • Simplify coverage.paths mapping to canonical src and site-packages locations ({{pkg}} and tests), ensuring data from .tox site-packages installations is unified
  • Extend coverage.report exclude_lines with patterns for PackageNotFoundError fallback, def main(, and async def run_server
  • Explain coverage strategy and pitfalls in CLAUDE.md, including the need to verify via tox run and filename-anchored omit patterns
template/pyproject.toml.jinja
CLAUDE.md
Mark inherently untestable launch/display code paths and C-extension fallback as excluded from coverage.
  • Annotate CLI interactive/gui/web commands with # pragma: no cover due to interactive/blocking behavior
  • Mark GUI _display_message, TUI _display_tui, worker lifecycle hooks, and C-extension ImportError fallback with # pragma: no cover
  • Update vulture configuration to ignore FastStream subscriber decorators and pytestmark when include_worker is enabled
template/src/{{github_repo_name}}/cli/app.py.jinja
template/src/{{github_repo_name}}/__init__.py.jinja
template/src/{{github_repo_name}}/gui/app.py.jinja
template/src/{{github_repo_name}}/tui/app.py.jinja
template/pyproject.toml.jinja
Document the worker testing and coverage strategy as ADR-008 and reference it throughout the template docs.
  • Add ADR-008 detailing the worker broker testing strategy, rationale for broker factory, testcontainers integration, and systemic coverage fixes
  • Cross-link ADR-008 from CLAUDE.md and README sections describing worker behavior, integration tests, and coverage exclusion conventions
docs/adr/008-worker-broker-testing-strategy.md
CLAUDE.md
template/README.md.jinja

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@hasansezertasan hasansezertasan added the no-issue Bypass the linked-issue requirement for PRs that need no issue label Jul 4, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The build_broker factory currently relies on a nested handle_version_request closure and an _ = handle_version_request reference to appease static analysis; consider exposing the handler as a named module-level function or adding a targeted vulture/ruff ignore instead of this indirection to keep the worker wiring easier to reason about and reuse.
  • The integration tox env relaxes deprecation warnings globally via -W default::DeprecationWarning, which can mask deprecations in your own code; you may want to narrow this to specific third-party modules (e.g., testcontainers and broker drivers) to keep template code honest while still preventing external noise from failing the run.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `build_broker` factory currently relies on a nested `handle_version_request` closure and an `_ = handle_version_request` reference to appease static analysis; consider exposing the handler as a named module-level function or adding a targeted vulture/ruff ignore instead of this indirection to keep the worker wiring easier to reason about and reuse.
- The `integration` tox env relaxes deprecation warnings globally via `-W default::DeprecationWarning`, which can mask deprecations in your own code; you may want to narrow this to specific third-party modules (e.g., testcontainers and broker drivers) to keep template code honest while still preventing external noise from failing the run.

## Individual Comments

### Comment 1
<location path="template/pyproject.toml.jinja" line_range="304" />
<code_context>
+# separate copy per env and a version-gated line covered in one interpreter but not
+# another is wrongly counted as missed — silently failing the `fail_under` gate.
+{{github_repo_name}} = ["src/{{github_repo_name}}", "*/site-packages/{{github_repo_name}}"]
+tests = ["tests", "*/site-packages/tests"]


</code_context>
<issue_to_address>
**suggestion (testing):** Reconsider mapping `tests` to `*/site-packages/tests` in `coverage.paths`.

The `tests` package is typically not installed into site-packages, so this mapping is likely unused and could unintentionally merge coverage from an unrelated installed `tests` package. Since `tests` is already listed as a source package and mapped to the repo-local `tests` directory, consider removing the `*/site-packages/tests` entry unless you have a specific case where your own tests are installed there.

Suggested implementation:

```
# Unify coverage data across environments. tox installs the built wheel/sdist into
# each env's site-packages, so the same source file is measured under a different
# `.tox/<env>/.../site-packages/{{github_repo_name}}` path per interpreter. Without
# remapping those back to the canonical `src/` tree, `coverage combine` keeps a
# separate copy per env and a version-gated line covered in one interpreter but not
# another is wrongly counted as missed — silently failing the `fail_under` gate.
{{github_repo_name}} = ["src/{{github_repo_name}}", "*/site-packages/{{github_repo_name}}"]

```

If `tests` needs to be treated as a source package for coverage, ensure it is listed under `[tool.coverage.run]` (e.g. `source = ["{{github_repo_name}}", "tests"]`) elsewhere in the template, without adding any `coverage.paths` remapping to `*/site-packages/tests`. This keeps coverage focused on the repo-local tests directory while avoiding accidental merging with unrelated installed `tests` packages.
</issue_to_address>

### Comment 2
<location path="template/pyproject.toml.jinja" line_range="279" />
<code_context>
+  # don't let `error` promote it to a collection/test failure (ADR-008). The
+  # integration module is imported during default collection even though its tests
+  # are deselected by the `integration` marker.
+  "ignore:The @wait_container_is_ready decorator is deprecated:DeprecationWarning",
+{%- endif %}
 ]
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Narrow the deprecation warning filter to just the testcontainers import site if possible.

This filter will silence any `DeprecationWarning` with that message, regardless of where it comes from. If testcontainers emits it from a consistent module path, consider using the full `action:message:category:module` form scoped to `testcontainers.*` to avoid hiding unrelated deprecations while still ignoring this specific library noise.

Suggested implementation:

```
  # testcontainers applies its own deprecated @wait_container_is_ready decorator at
  # import time. The warning is internal to the library and not actionable here, so
  # don't let `error` promote it to a collection/test failure (ADR-008). The
  # integration module is imported during default collection even though its tests
  # are deselected by the `integration` marker.
+  "ignore:The @wait_container_is_ready decorator is deprecated:DeprecationWarning:testcontainers.*",

```

```
  "error",
  # It is harmless for tests, so don't let `error` promote it to a failure.
  "ignore:Using `httpx` with `starlette.testclient` is deprecated:Warning",
{%- endif %}
{%- if include_worker %}
  # testcontainers applies its own deprecated @wait_container_is_ready decorator at
  # import time. The warning is internal to the library and not actionable here, so
  # don't let `error` promote it to a collection/test failure (ADR-008). The
  # integration module is imported during default collection even though its tests
  # are deselected by the `integration` marker.
  "ignore:The @wait_container_is_ready decorator is deprecated:DeprecationWarning:testcontainers.*",

docs = [

```

If the actual module path for the deprecation is known (e.g. `testcontainers.core.waiting_utils`), you can replace `testcontainers.*` with the more precise module name to further narrow the filter:
`"ignore:The @wait_container_is_ready decorator is deprecated:DeprecationWarning:testcontainers.core.waiting_utils"`.
</issue_to_address>

### Comment 3
<location path="docs/adr/008-worker-broker-testing-strategy.md" line_range="8" />
<code_context>
+Accepted (2026-06). Implemented.
+
+> **Revision (during implementation).** The original draft proposed *lazy broker
+> imports* as item 1. Implementation showed that buys nothing for this worker:
+> it has exactly one broker, `faststream[<broker>]` is a mandatory dependency
+> (so the driver is always installed), and the broker is constructed at module
</code_context>
<issue_to_address>
**issue (typo):** Sentence "Implementation showed that buys nothing for this worker" is missing a word.

Consider rephrasing to make the sentence grammatical, e.g. "Implementation showed that this buys nothing for this worker" or "Implementation showed that it buys nothing for this worker."

```suggestion
> imports* as item 1. Implementation showed that this buys nothing for this worker:
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread template/pyproject.toml.jinja Outdated
Comment thread template/pyproject.toml.jinja Outdated
Comment thread docs/adr/008-worker-broker-testing-strategy.md Outdated
Review fixes from the multi-agent review pass and Sourcery
(sourcery-ai review 4630116841):

Rendered-output bugs:
- README: render TestRabbitBroker (not the nonexistent TestRabbitmqBroker)
  for the rabbitmq broker via the same rabbit mapping used for the
  faststream extra.
- Worker unit tests: restore the parenthesized multi-line import so repo
  names longer than 11 chars stay under ruff's 88-char limit (the
  single-line form broke the generated style gate).

Coverage honesty (replaces blanket exclude_lines with per-site pragmas):
- Drop the 'def main\(', 'async def run_server', and
  'except PackageNotFoundError' exclude_lines patterns: a regex matching a
  def line excludes the whole function body, which silently un-measured the
  tested GUI/TUI entrypoints and the web 503 handlers.
- Add targeted `# pragma: no cover` to the genuinely blocking entrypoints
  (web/MCP/worker main, MCP run_server, __main__ dispatchers) and the
  worker's module-level metadata fallback (now with a warning log).
- Test the reachable metadata-failure paths instead: web /version + /info
  503s (fastapi + litestar), CLI exit-code-1 contract, GUI/TUI
  "Version: unknown" degradation, MCP error text — via a
  _MissingDistribution monkeypatch stub and importlib module handles (the
  cli/web/worker __init__ re-export `app`, shadowing the submodule).
- Cover the GUI/TUI main() success paths (previously masked by the blanket
  pattern) with stubbed display helpers.

Worker tests:
- Assert build_broker actually forwards the explicit URL (constructor spy)
  and add the env-var fallback test — previously a dropped url parameter
  would only surface in the Docker-only CI job.
- Add a malformed-request negative test (ValidationError, no response).
- Drop the tautological test_broker_exists.
- Replace the `_ = handle_version_request` indirection with a targeted
  pyright ignore naming the tool (basedpyright reportUnusedFunction).

Integration test hardening:
- Publish inside the poll loop: kafka/redis/nats subscriptions are
  at-most-once from a cold consumer, so a single up-front publish was a
  built-in race on the only real-driver CI job.
- Bound broker.start() with asyncio.wait_for and each test with
  pytest-timeout (--timeout=300) so hangs fail with a traceback instead of
  the 15-min CI job kill.
- Give kafka a larger poll window (consumer-group join on cold runners).
- Assert the full response payload, not just the correlation id.

Config cleanups:
- Anchor the integration omit glob to */worker/test_integration.py.
- Drop the dead tests site-packages remap in coverage.paths.
- Drop the no-op rabbitmq conditional in the testcontainers extra.
- Scope the wait_container_is_ready filterwarnings entry to the
  testcontainers module (ini filters are regexes). The integration env's
  -W demotion stays broad: CLI -W module matching is exact and drivers
  attribute warnings via stacklevel to arbitrary caller modules (verified
  empirically with the redis driver).

Docs:
- ADR-008: fix the faststream dependency claim (extra, not mandatory),
  the Context/Decision contradiction about the unit-test rewrite, the
  url-or code sample, and document the pragma-over-blanket rationale and
  the -W scoping rejection; grammar fix from Sourcery.
- CLAUDE.md: rewrite the coverage exclusion convention for the per-site
  pragma approach.

Verified: unit suites for all four brokers, real-container integration
round-trips for all four brokers, 100% coverage + green style gate on the
all-components (rabbitmq, 20-char name), litestar-web, and base projects.
@hasansezertasan
hasansezertasan merged commit 97f02e4 into main Jul 5, 2026
10 checks passed
@hasansezertasan
hasansezertasan deleted the feat/worker-broker-testing branch July 5, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-issue Bypass the linked-issue requirement for PRs that need no issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant