Skip to content

feat(bridge): add Mockery bridge (testo/bridge-mockery) - #254

Merged
roxblnfk merged 4 commits into
php-testo:1.xfrom
rossaddison:feature/bridge-mockery
Jul 3, 2026
Merged

feat(bridge): add Mockery bridge (testo/bridge-mockery)#254
roxblnfk merged 4 commits into
php-testo:1.xfrom
rossaddison:feature/bridge-mockery

Conversation

@rossaddison

Copy link
Copy Markdown
Contributor

Closes#41

What this adds

A new bridge/mockery/ package (testo/bridge-mockery) that integrates Mockery into Testo.

How it works

MockeryPlugin registers MockeryInterceptor as a TestRunInterceptor. The interceptor wraps every test in a try/finally and calls Mockery::close() after each one — whether it passed or failed. This ensures:

  • Unfulfilled mock expectations are reported as test failures.
  • The Mockery container is cleared between tests, preventing state leaking from one test into the next.

Usage

// testo.phpreturnnewApplicationConfig(
plugins: [new \Testo\Bridge\Mockery\MockeryPlugin()],
suites: [newSuiteConfig(name: 'Unit', location: ['tests/Unit'])],
);
#[Test]
finalclass MyServiceTest
{
publicfunctioncallsRepository(): void
{
$repo = \Mockery::mock(UserRepository::class);
$repo->expects('find')->with(1)->andReturn(newUser(1));
$service = newUserService($repo);
$service->getUser(1);
// Mockery::close() is called automatically — expectation is verified here
}
}

Files

FilePurpose
bridge/mockery/composer.jsonPackage definition (testo/bridge-mockery)
bridge/mockery/src/MockeryPlugin.phpPluginConfigurator — entry point users register
bridge/mockery/src/Internal/MockeryInterceptor.phpTestRunInterceptor — calls Mockery::close() in finally
bridge/mockery/tests/Acceptance/MockeryBridgeTest.phpAcceptance tests: mock, spy, container isolation

The bridge follows the same structure and conventions as the existing bridge/symfony-console and plugin/lifecycle packages.

Closesphp-testo#41
Implements MockeryPlugin + MockeryInterceptor to call Mockery::close()
in a try/finally after every test, ensuring mock expectations are always
verified and the Mockery container is cleared between tests.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a new testo/bridge-mockery package that integrates Mockery into Testo by registering a TestRunInterceptor intended to call Mockery::close() after each test, plus initial acceptance tests and root composer wiring.

Changes:

  • Add testo/bridge-mockery to the monorepo (package composer.json, plugin, interceptor, changelog).
  • Wire the new bridge into the root repo dev dependencies / suggestions and test autoloading.
  • Add initial acceptance tests and a dedicated suites definition for the bridge.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
composer.jsonAdds the bridge as a dev dependency + suggestion, and adds test autoload mapping.
bridge/mockery/composer.jsonDefines the new testo/bridge-mockery package and its dependencies.
bridge/mockery/src/MockeryPlugin.phpPlugin entry point registering the interceptor into the pipeline.
bridge/mockery/src/Internal/MockeryInterceptor.phpInterceptor intended to ensure Mockery::close() runs after each test.
bridge/mockery/tests/suites.phpDeclares the acceptance test suite for the bridge package.
bridge/mockery/tests/Acceptance/MockeryBridgeTest.phpAcceptance tests demonstrating mock/spy usage and intended teardown behavior.
bridge/mockery/CHANGELOG.mdAdds initial changelog for version 0.1.0.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadbridge/mockery/src/Internal/MockeryInterceptor.php
Comment threadbridge/mockery/tests/suites.php
Comment threadbridge/mockery/tests/Acceptance/MockeryBridgeTest.php Outdated
@roxblnfk
roxblnfk self-requested a review July 3, 2026 08:11
roxblnfkand others added 3 commits July 3, 2026 12:42
…ests
- register the package in release-please (config, version.json seed, split-publish tag)
- add mirror README and close-prs workflow to match the other bridges
- reduce CHANGELOG to a stub so release-please manages it
- wire the acceptance suite into testo.php and register MockeryPlugin on it
- strengthen the acceptance test: #[Covers] + assert the container is reset,
and drop the risky spy test by adding a real assertion
- align the composer `suggest` wording with the infection entry
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… as assertions
- MockeryInterceptor records verified expectations (ExpectationFulfilled) and unmet
ones (ExpectationFailed) on the current test, so a mock-only test is not Risky and
an unmet expectation fails the test instead of aborting the pipeline
- guard the process-global Mockery container across fiber suspensions with the same
save/reset/restore dance as MessengerHub::scope
- tune ExpectationsInterceptor order to ORDER_ASSERTIONS + 2000 so the Assert plugin
reads the history after the bridge records into it
- add Self / Feature / Stub tests covering mock+assert combinations and statuses
- declare testo/assert, testo/codecov, testo/test as dev dependencies
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover the failure path of the teardown: a stub that leaves an unmet expectation
(Failed) is followed by one that checks the container is empty at its start
(Passed). The latter only passes if close() cleared the container despite the
failure — proving the interceptor both fails the right test and resets state for
subsequent tests, not only when a test passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecovBot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
bridge/mockery/src/MockeryPlugin.php0.00%1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@roxblnfk
roxblnfk merged commit 6f4596e into php-testo:1.xJul 3, 2026
12 of 14 checks passed
@roxblnfkroxblnfk mentioned this pull request Jul 3, 2026
@roxblnfk

Copy link
Copy Markdown
Member

The docs is there https://php-testo.github.io/docs/bridge/mockery
Thanks

@rossaddison

Copy link
Copy Markdown
ContributorAuthor

Installed and wired up in a production Yii3 invoice application today. Setup was exactly as documented — one plugin registration in testo.php and no per-test teardown in any test class.

First real-world test suite — three scenarios for As4RetryEngine::detectMissingReceipts(), all running against interface mocks with no database:

#[Test]
finalclass As4RetryEngineTest
{
publicfunctiondetectMissingReceiptsReturnsZeroForEmptyQueue(): void
{
$repo = m::mock(As4MessageRepositoryInterface::class);
/** @var \Mockery\Expectation $e */$e = $repo->shouldReceive('findAwaitingReceipts');
$e->once()->andReturn([]);
$engine = newAs4RetryEngine($repo, m::spy(As4SenderInterface::class), ...);
Assert::same(0, $engine->detectMissingReceipts());
}
// ...
}

One Psalm note worth documenting: shouldReceive() is typed as returning Expectation|ExpectationInterface|HigherOrderMessage. At Psalm errorLevel 1, calling ->once() or ->andReturn() on that union fails because not every member of the union declares those methods. The workaround is a @var \Mockery\Expectation annotation on the intermediate variable before chaining. Not a blocker, but a note for anyone running strict Psalm in their test suite.

The spy pattern for don't-care dependencies (m::spy(LoggerInterface::class)) was particularly clean — no stub boilerplate for collaborators that aren't under test.

@rossaddison
rossaddison deleted the feature/bridge-mockery branch August 13, 2026 15:10
rossaddison added a commit to rossaddison/testo that referenced this pull request Aug 13, 2026
…CapturedErrors to public
Addresses roxblnfk's review on php-testo#262:
- set_error_handler()/restore_error_handler() operate on one process-global
stack. The old code installed its handler once before $next() and
restored once after — but $next() can suspend a fiber mid-test while a
sibling test interleaves, so the handler stayed installed (and, on an
interleaved resume, restore_error_handler() could pop a sibling's frame
instead of its own). Same defect as php-testo#254.
Fixed by wrapping $next() in its own fiber and swapping the handler on
every suspend/resume — restore (native stack pop) on suspend, reinstall
on resume — mirroring the already-reviewed pattern in
MockeryInterceptor::run() and MessengerHub::scope().
Regression test added (restoresTheOuterHandlerWhileSuspendedAndReinstallsItsOwnOnResume):
confirmed it fails against the old code (an error fired while suspended
was wrongly captured by this test's own handler instead of reaching the
outer one) and passes against the fix.
- Promoted Internal\CapturedErrors to a public Testo\ErrorHandler\CapturedErrors
class (Copilot review comment): the plugin's own docs already tell
consumers to read this attribute off TestResult, so it was never really
internal — it just wasn't marked as such. Fixes the @api-marked
ErrorHandlerPlugin's docblock referencing an internal type.
- The other Copilot comment (restrict the failOnError status upgrade to
Status::Passed) was already fixed in a prior commit on this branch — no
change needed.
Also rebased onto current 1.x (26 commits behind), resolving one real
conflict in composer.json (version bumps landed upstream since this PR
opened) and the same CaseDefinition/CaseInfo required-argument fix already
applied on php-testo#264.
The second question from review — what should happen if a test changes
the error handler itself mid-run — is intentionally left open; per
roxblnfk's own comment it needs research/discussion before implementing,
not a quick fix.
Verified:
- composer rector:ci: clean, 0 files
- Full Testo suite: 1682 passed, 6 failed/7 error (same pre-existing
Bench/Self baseline as the current rector/* PR series, unrelated)
- ErrorHandlerInterceptorTest: 11/11 passed, including the new
fiber-safety regression test (confirmed it fails against the old code)
- Psalm: this repo's Psalm CI only covers core/ (confirmed via psalm.xml
and psalm.yml's trigger paths) — plugin/error-handler/ was never in
scope, unchanged by this fix
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for freeto 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.

Mockery

3 participants

@rossaddison@roxblnfk