Add test infrastructure: PHPUnit harness, CI, and scoped PHPCS - #37
Open
coraislovely-code wants to merge 2 commits into
Open
Add test infrastructure: PHPUnit harness, CI, and scoped PHPCS#37coraislovely-code wants to merge 2 commits into
coraislovely-code wants to merge 2 commits into
Conversation
The theme has never had a test of any kind. The obstacle has always been that testing a WordPress theme appears to require WordPress, and therefore a database. It does not, for the part that matters. The theme's logic -- escaping decisions, SQL construction, nonce and capability checks on the options screen -- depends only on its arguments plus a small set of WordPress helpers. Stubbing those gives a suite that runs in milliseconds with no database, no Docker and no WordPress checkout. Several of the stubs have to be faithful or the tests they support quietly stop meaning anything, and this is documented at the top of tests/stubs.php: - esc_html()/esc_attr() call _wp_specialchars() with $double_encode = false, so they leave existing entities alone, while esc_textarea() double-encodes. Much of this theme's escaping behaviour turns on that difference, so a naive htmlspecialchars() stub would give the wrong answer. - esc_url() drops a disallowed scheme and esc_attr() does not. An href escaped with esc_attr() is well-formed HTML and a working javascript: URL, and several places in the theme do exactly that, so the two stubs must not be interchangeable. - wp_filter_nohtml_kses() is the only sanitiser the options screen uses. WordPress defines it as addslashes( wp_kses( stripslashes( $data ), array() ) ): it removes every tag and leaves quotes ALONE. A stub that also escaped quotes would make unescaped attribute output look safe. - The nonce functions are a test-driven seam, not a reimplementation. A test can make verification pass, make it fail, and -- the part that matters -- see whether it was attempted at all, because a handler with no nonce check passes every happy-path test. - current_user_can() defaults to false, so a missing capability check fails a test instead of sailing through one. Also included is a $wpdb spy that records the SQL it is handed, which is what allows query construction to be tested without a database. It answers $wpdb->escape() as well, removed from WordPress in 5.3 but still called by widgets/calendar.php. Loading theme code needs one wrinkle. functions.php cannot be required from a test: it calls easel_themeinfo() before defining it, glob-autoloads all of functions/ and widgets/ through get_template_part(), and pulls in options.php under is_admin(). So tests require one file at a time, and easel_themeinfo()/easel_load_options() are stubbed in the bootstrap. Those two are worth testing themselves, so the stubs are dispatchers: Easel_TestCase::loadRealThemeInfo() lifts the real bodies out of functions.php verbatim and switches both over together. No theme file is modified -- the harness has to be able to test the theme as shipped. CI runs php -l across 7.4 through 8.4, PHPUnit on 8.2 through 8.4, and an advisory PHPCS job scoped to the lines a pull request adds rather than the files it touches. The theme carries roughly six hundred pre-existing findings; a per-file run would be red by default, and a job that is always red is a job nobody reads. HarnessTest.php asserts these properties of the harness itself. If it fails, nothing else in the suite should be trusted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These pin the theme's current behaviour rather than the behaviour it ought to have, so that when the escaping, the request handling and the option loading are changed the assertions have to be inverted rather than merely kept passing. Every test says in its docblock what it is recording and what the change will turn it into. Covered: how author contact details and the menubar social links are concatenated into markup, the order in which the options screen reads the request against its form token, the sanitize_callback names the customizer registers and the type of its three width settings, the default array built on a site that has never pressed Save, and two $_SERVER reads that assume the key is present. The harness grows three things the new tests needed: a recorder that stands in for $wp_customize, a helper that lifts a single function out of functions.php, and a helper that buffers output and collects the diagnostics a call raises so a test can assert on them instead of the run failing on them. No theme file is touched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This theme has had no automated checks of any kind. This adds them without changing a single line of theme code — every file in this pull request is new.
It is the first of three; the two that follow contain actual changes and are stacked on this one.
A PHPUnit harness that does not boot WordPress
The theme's functions depend on their arguments plus a small set of WordPress helpers, so
tests/stubs.phpsupplies those helpers and the tests require theme files directly. No database, no WordPress checkout, no fixtures — the suite runs in well under a second.A few stubs are deliberately faithful rather than convenient, because tests hang on the difference:
esc_html()/esc_attr()do not re-encode existing entities;esc_textarea()does.esc_url()refuses a scheme thatesc_attr()passes straight through.wp_filter_nohtml_kses()— the options screen's only sanitiser — strips tags but leaves quotes alone.current_user_can()defaults to false, so a missing capability check surfaces as a failing test rather than a silent pass.tests/HarnessTest.phpasserts each of those. If the harness ever drifts, that file fails first rather than the rest of the suite quietly reporting nonsense.Characterization tests
The second commit pins current behaviour — including several things that are currently wrong — so that the changes in the follow-up pull requests visibly flip an assertion instead of being taken on trust. Each docblock states what the assertion becomes afterwards.
CI
php -lon 7.4 through 8.4. The cheapest useful signal here: recent work on this theme has been PHP 8 compatibility, and until now nothing verified it on more than one interpreter.That last choice is deliberate. The theme predates the WordPress coding standards by about a decade and carries several hundred pre-existing findings, so a per-file run would go red on any pull request touching legacy code whether or not it made anything worse — and a job that is red by default is one everyone learns to scroll past.
.github/scripts/phpcs-added-lines.pymaps the report onto the diff and reports only added lines, printing a count of what it suppressed so the backlog stays visible rather than forgotten.It is advisory because some of what it reports cannot reasonably be fixed: the sniffs cannot see through
apply_filters()to an escaper inside it, cannot know an interpolatedORDER BYdirection was whitelisted, and object to the$before_title/$after_titlethat every WordPress widget emits. Pass--strictto make it blocking if that is ever wanted.Verification
Ten jobs, all green on their first run — six
php -l, three PHPUnit, one PHPCS.composer testreportsOK (48 tests, 178 assertions)on this branch.