2.9 KiB
2.9 KiB
Test Guard — PHP / PHPUnit / Pest Patterns
Concrete applications of the nine rules for PHP projects, including WordPress and WooCommerce. Read this when reviewing or writing PHP tests.
Rule 2: Mock boundaries in PHP
Justified mock targets:
- HTTP: Guzzle handlers/middleware,
pre_http_requestfilter in WordPress - External SDKs: payment gateways, mail providers, LLM API clients
- Clock: inject a clock (
psr/clock) instead of callingtime()directly - Filesystem on external paths (prefer
vfsStreamor temp dirs over mocking)
Unjustified mocks:
- Mockery/Prophecy doubles for the project's own value objects, DTOs, or entities — construct real instances (Rule 8)
- Mocking internal services just to isolate a class — if wiring is painful, fix the constructor, don't fake the collaborator
- Partial mocks of the class under test — you are no longer testing the class
Rule 3: Data providers
/**
* @see Rule 3 — variants of one scenario belong in a data provider.
*/
#[DataProvider('provideSlugCases')]
public function test_slugify_normalizes_input( string $raw, string $expected ): void {
$this->assertSame( $expected, slugify( $raw ) );
}
public static function provideSlugCases(): array {
return array(
'lowercases' => array( 'Hello World', 'hello-world' ),
'strips padding' => array( ' padded ', 'padded' ),
'transliterates' => array( 'Café Menu', 'cafe-menu' ),
);
}
Pest equivalent: it('normalizes slug', ...)->with([...]).
WordPress-specific boundaries
- Integration tests (
WP_UnitTestCase/wp-env/wp-cli scaffold): use the real WordPress test framework with factories —self::factory()->post->create(),self::factory()->user->create(). Don't mockWP_PostorWP_User; the factories exist precisely so you don't have to (Rule 8). - Unit tests without WordPress loaded (Brain Monkey / WP_Mock): mocking WordPress functions like
get_option()orapply_filters()is a boundary mock and justified. But assert what your code does with the values, not thatget_optionwas called with specific args (Rule 1). - Mock outbound HTTP with the
pre_http_requestfilter rather than patchingwp_remote_getinternals. - Don't test that WordPress sanitizes, escapes, or that hooks fire — that's core's guarantee (Rule 7). Test your callback's behavior given an input.
WooCommerce notes
- Build real
WC_Product/WC_Orderobjects viaWC_Helper_ProductandWC_Helper_Orderin integration tests — neverMagicMock-style doubles of them (Rule 8). - Cart and checkout logic is stateful: prefer integration tests over heavily mocked unit tests; mocked carts hide hook-ordering bugs.
Rule 9: Real database
WP_UnitTestCase already wraps each test in a transaction against a real schema — use it for query, meta, and persistence logic instead of mocking $wpdb. Mocking $wpdb->prepare or $wpdb->get_results to test a query builder tests nothing.