Skip to content

Add sync/async behavioral parity tests - #321

Merged
Mattsface merged 5 commits into
release/1.1.0from
test/304-sync-async-parity
Aug 22, 2026
Merged

Add sync/async behavioral parity tests#321
Mattsface merged 5 commits into
release/1.1.0from
test/304-sync-async-parity

Conversation

@Mattsface

Copy link
Copy Markdown
Member

Why

Add deterministic coverage to protect against behavioral drift between the synchronous Mlb client and the new AsyncMlb client.

The sync client is the compatibility baseline. These tests verify that equivalent inputs and responses produce equivalent caller-visible behavior without requiring the sync and async transports to have identical internals.

Closes #304

What

Added sync/async parity tests covering:

  • get_team
  • get_person
  • get_schedule
  • Successful model parsing and returned values
  • Equivalent request paths and parameters
  • Empty and 404 behavior
  • Strict non-404 4xx behavior
  • strict_http=False compatibility behavior
  • Representative 5xx behavior
  • Timeout and transport exceptions
  • Invalid successful JSON
  • Structured MlbHttpError context

Shared transport failures are tested through one representative endpoint rather than duplicating the same error matrix for every endpoint.

No production code was changed.

Tests

Tested with the deterministic parity suite:

poetry run pytest tests/test_sync_async_parity.py

Also ran the full offline test suite:

poetry run pytest tests/ --ignore=tests/external_tests

And checked the diff with:

git diff --check

No live MLB API calls are used by the new parity tests.

Risk and impact

Risk: Minimal

This PR only adds deterministic tests and does not change production behavior or the public API.

If something does go wrong, the likely impact would be an overly strict or incorrect test causing CI failures. Runtime library behavior for users would not be affected.

claude and others added 5 commits August 21, 2026 19:05
Batch 1 of issue #304. Adds offline parity tests that drive the public Mlb
and AsyncMlb clients over equivalent canned responses and compare only what
a caller can see:

- successful 2xx produces the same model type and parsed values
- a successful empty response returns None on both clients
- a 404 returns None on both clients

Transport behavior is already covered elsewhere, so nothing here compares
Requests and HTTPX internals. Later #304 batches cover get_schedule, strict
non-404 4xx, compatibility mode, 5xx, timeout, transport and decode errors.

Refs #304

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYDDda51C9LGsiv2cS9Pkr
Restructure the #304 parity tests around a single `call_both` helper that
always captures and compares both clients' requests, rather than an opt-in
`request_signatures` list threaded through three helper signatures.

Every parity test now checks request parity, not just the two get_schedule
cases. Injecting endpoint drift into AsyncMlb.get_team is caught by six tests
instead of two: the MockTransport handler answers any path, so a wrong async
endpoint previously slipped past the success and no-result tests and showed up
only in the URL carried by MlbHttpError.

Other cleanups:

- Hold the no-result responses as the keyword arguments that produce them, so
  the empty-body case folds into the table and covers get_person and
  get_schedule too. The schedule table now extends the shared one with its
  empty-envelope case instead of restating 404.
- Table-drive the two canned transport failures per client, replacing the
  branch-per-failure dispatch duplicated in both helpers. The async side now
  rejects an unknown failure name at the same point the sync side does,
  instead of lazily inside the transport handler.
- Merge the two MlbHttpError tests and the two transport-failure tests into
  parametrized pairs, and share the pytest.raises pairing in `raise_both`.
  The exact-type assertion stays: MlbTimeoutError subclasses
  MlbTransportError, which pytest.raises alone would not distinguish.
- Drop the duplicated per-field assertions on the async result. Pydantic
  equality compares the model class, so asserting the sync type plus
  cross-client equality pins the async type and every field.
- Default `status` to 200 so failure cases stop passing a status that is
  never used, and build the httpx client inside the coroutine.

424 lines and 20 tests, from 531 lines and 17 tests. Full offline suite: 765
passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqA6vNdLdccVfTPB8Dj9yq
@Mattsface

Copy link
Copy Markdown
Member Author

@claude take a look

@claude

claude Bot commented Aug 22, 2026

Copy link
Copy Markdown

Claude finished @Mattsface's task in 1m 39s —— View job


Review of tests/test_sync_async_parity.py

  • Gather context (read PR diff, repo structure, CLAUDE.md)
  • Understand the request (code review of new parity test file)
  • Review tests/test_sync_async_parity.py for correctness, coverage, and style
  • Verify test correctness against implementation (mlb_api.py, async_mlb.py, _http.py, mlb_dataadapter.py, async_mlb_dataadapter.py)
  • Post review feedback

Note: I wasn't able to execute poetry run pytest directly in this sandboxed session (Bash execution required approval that wasn't available). I instead traced the implementation to confirm correctness by hand, and confirmed the Offline CI workflow run for this PR already passed (https://github.com/zero-sum-seattle/python-mlb-statsapi/actions/runs/32575527656), which runs tests/ --ignore=tests/external_tests and includes this new file.

Overall

This is a well-designed, focused test file. A few things stood out positively:

  • The scope is deliberately narrow (public-behavior parity only), and that's enforced by the design of call_both/raise_both rather than just asserted in the docstring.
  • request_signature (tests/test_sync_async_parity.py:108) normalizing method/path/query is a nice way to compare requests vs httpx request objects without coupling to transport internals.
  • call_both (tests/test_sync_async_parity.py:206) asserting exactly one outbound request on the sync side and identical requests on the async side is a good guard against a client silently fanning out multiple calls.
  • I traced the strict/compatibility 4xx logic through mlbstatsapi/_http.py and both mlb_dataadapter.py / async_mlb_dataadapter.py: strict_http=True (default) raises MlbHttpError for any non-404 4xx and all 5xx at the adapter layer, and strict_http=False emits MlbHttpCompatibilityWarning and falls through as None. The tests at tests/test_sync_async_parity.py:372 and :392 correctly reflect this, and the 400 <= status_code <= 499: return None fallback in mlb_api.py's get_team/get_person is effectively 404-only dead-letter handling once strict mode has already raised — the tests correctly target that boundary.
  • Caller-owned session/client cleanup semantics (library never closes an injected requests.Session/httpx.AsyncClient) are correctly matched by the manual session.close() / client.aclose() in call_sync/call_async, consistent with Mlb.close() (mlbstatsapi/mlb_api.py) and AsyncMlb.aclose() (mlbstatsapi/async_mlb.py).
  • pytest.importorskip("httpx", ...) at module level correctly keeps this file a no-op on a sync-only install, matching the sync-only CI job.

Minor / optional observations (non-blocking)

  • tests/test_sync_async_parity.py:392 (test_get_team_compatibility_client_error_parity) only checks len(sync_warnings) == len(async_warnings) == 1, not the warning message/category content itself beyond pytest.warns(MlbHttpCompatibilityWarning). That's consistent with the file's stated scope (compare only caller-visible behavior), so this is a judgment call, not a bug.
  • The raise_both helper (tests/test_sync_async_parity.py:226) pins type(sync_exc.value) is type(async_exc.value) is expected, which is a nice touch to prevent a subclass (e.g. MlbTimeoutError vs MlbTransportError) from masking a real behavioral difference.
  • No production code changes, consistent with the PR description.

No changes requested — this looks solid and CI is green.

@Mattsface
Mattsface merged commit 13e3cc6 into release/1.1.0 Aug 22, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants