Skip to content

test: migrate the test suite from PHPUnit to Testo - #112

Merged
roxblnfk merged 6 commits into
1.xfrom
testo
Aug 11, 2026
Merged

test: migrate the test suite from PHPUnit to Testo#112
roxblnfk merged 6 commits into
1.xfrom
testo

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Aug 11, 2026

Copy link
Copy Markdown
Member

🔍 What was changed

  • The whole test suite runs on Testo instead of PHPUnit: phpunit.xml.disttesto.php with one suite per former testsuite, PHPUnit gone from composer.json. 456 tests — 453 passed, 3 skipped, none deleted or weakened.
  • Mocks moved to Mockery through testo/bridge-mockery (Testo core ships none). MockeryPlugin verifies expectations and clears the container after every test, so no teardown boilerplate and mock-only tests stay out of Status::Risky.
  • tests/Arch is dropped. Its only check was a forgotten-debug-function list that Psalm's forbiddenFunctions already covered, except dump and error_log — both added to psalm.xml, neither used in src.
  • CI: the phpunit.json problem matcher is removed (Testo ships none), Codecov reads runtime/coverage/clover.xml, and PHP 8.1 leaves the matrix — it was already below the >=8.2 requirement.

Why?

Two things are worth a reviewer's attention, both places where the automated conversion was wrong rather than merely incomplete:

  • Exception messages. PHPUnit's expectExceptionMessage() matches a substring, Testo's withMessage() matches exactly. The Rector bridge emits the latter, which silently broke 15 cases — e.g. expecting 'Version constraint cannot be empty' against a SUT throwing the same string with a trailing period. 8 sites are now withMessageContaining(); the rest were exact already and keep the stronger assertion.
  • ClientStub keys on the request instance. It used to build its key from getMethod() and getUri(), which only worked because PHPUnit's createMock() returns a cached auto-double per method, collapsing every unconfigured request into the single key '|'. Mockery returns a fresh double per call. The key is now spl_object_id($request), independent of any mock's defaults. This is also why two tests in ClientTest always compared with assertEquals rather than assertSame.

The bridge gaps found on the way are reported upstream in php-testo/testo#270: the withMessage mistranslation, the assertions missing from its map (assertStringContainsString alone was 43 call sites here), and that the mock conversion is in fact mechanical against Mockery.

Checklist

  • How was this tested:
    • Full suite green locally — 453 passed, 3 skipped of 456
    • testing matrix green in CI across PHP 8.2–8.5 × lowest/locked/highest
    • psalm is red, and was already red on 1.x before this branch — every error is in src/ files this PR does not touch, and none is a ForbiddenCode from the psalm.xml change
    • code-coverage is red because the Acceptance suite downloads from the GitHub API anonymously and hit rate limit exceeded (limit: 60 requests per hour) after the 12 testing jobs; unrelated to the migration, but the suite needs a GITHUB_TOKEN in CI to be reliable

Review notes

Four commits, each verifiable on its own: b8fed24 runner config · 5b86ef7 the mechanical Rector pass · f8359e0 what Rector left behind, with test counts and results unchanged · 273bfcb mocks, the unmapped assertions and the message fixes.

#[Test] sits on the methods rather than the class. Testo prefers a single class-level marker when every public method is a test, which holds for essentially all of these classes — happy to hoist it here or leave it as a follow-up.

ci: point the coverage upload at Testo's clover report

chore(psalm): forbid dump() and error_log()

Suites in testo.php mirror the phpunit.xml.dist testsuites. No plugins are
declared: Testo's defaults already ship an inert JUnit and Codecov plugin that
the `--log-junit=` / `--coverage-*` flags activate, so report paths stay in the
composer scripts instead of the config.

The Arch suite goes away with phpunit-architecture-test. Its only check was a
list of forgotten debug functions, which psalm's forbiddenFunctions already
covered except for dump() and error_log() — hence the two additions there.

The tests themselves are still PHPUnit-based, so nothing is discovered yet;
phpunit.xml.dist stays until they are ported.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checkpoint commit: `testo/bridge-rector`'s phpunit-to-testo set converted the
assertions (with the expected/actual swap), lifecycle methods, data providers,
coverage attributes and the TestCase base class. The structural residue — mocks,
non-mapped assertions, method renames — is finished in the next commits.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops the leftover `use PHPUnit\…;` imports, turns the fully-qualified `\Testo\…`
names the bridge emits into real imports, renames `testFoo()` → `foo()` and the
PHPUnit lifecycle methods, and strips the Arrange/Act/Assert labels — Testo
separates phases with a blank line. Test counts and results are unchanged by
this commit; the remaining failures are mocks and unmapped assertions.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test(velox): key the GitHub client stub on the request instance

Testo ships no mocking, so `MockeryPlugin` is registered in `testo.php` and the
`createMock()` doubles became `\Mockery::mock()`. Placeholder doubles that were
never configured get `shouldIgnoreMissing()` — that is what PHPUnit's
`createMock()` did, and `ClientStub` relied on it: it derived its lookup key from
`getMethod()`/`getUri()`, which collapsed every unconfigured request into one
bucket. The key is now the request instance itself. The PSR-18 client exception
stays a real exception class: Mockery cannot mock a `Throwable` descendant.

Also closes the two conversions the Rector bridge does not do — the assertions
missing from its map (`assertStringContainsString` and friends) and the exception
messages, where the bridge emits the exact-match `withMessage()` for PHPUnit's
substring `expectExceptionMessage()`. Both are reported upstream in
php-testo/testo#270.

Whole suite green under Testo: 453 passed, 3 skipped. `phpunit.xml.dist` and the
disposable Rector config are gone.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: drop PHP 8.1 from the testing matrix

`--color=always` was carried over from the PHPUnit scripts; Testo has no such
option and every `composer test` run failed on it. PHP 8.1 was already below the
`>=8.2` requirement in composer.json, so those matrix jobs could not install.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The source message ends with a period while the expectation does not, and the
Testo withMessage() is an exact match where the PHPUnit expectExceptionMessage()
was a substring one. Not caught locally: the test skips itself on Windows, where
chmod cannot make a file unreadable.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
see 115 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@roxblnfk
roxblnfk merged commit 84291ed into 1.x Aug 11, 2026
34 of 36 checks passed
@roxblnfk
roxblnfk deleted the testo branch August 11, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant