Skip to main content

Automated Hook Tests with WP_UnitTestCase

Hook behavior should be verified with repeatable assertions, not inferred from manual editing only.

Learning Focus

You will write unit and integration tests for ACF value filters and acf/save_post synchronization routines.

Concept Overview

ACF hook tests usually fall into two categories: transformation tests (acf/update_value, acf/format_value) and workflow tests (acf/save_post, relation sync, derived metadata updates). WP_UnitTestCase helps build realistic WordPress context while keeping assertions deterministic.

Core Idea

For every business-critical hook, define input, lifecycle stage, and exact expected output in one test method.

Why It Matters

Untested hooks can silently mutate stored data and break templates days after release.

ApproachWhat HappensBusiness Impact
Test hook callbacks with deterministic fixturesBehavior remains stable across refactorsSafer long-term maintenance
Validate only by clicking admin UIRegressions are discovered lateHigher release risk
Add integration tests for acf/save_post flowsCross-record sync issues are caught earlyFewer data integrity incidents

Reference Table

TermMeaningWhere You Use ItFast Check
WP_UnitTestCaseWordPress testing base classUnit and integration tests in plugin/theme reposTest class boots WP context correctly
Hook AssertionTest verifying callback output or side effectsValue filters and save automationAssertion checks exact expected value
Integration FixtureSeeded posts/users/terms for workflow testsacf/save_post and relation syncFixture IDs are deterministic in test scope
Regression TestTest written after bug discoveryPrevent repeat failures in future releasesTest fails on old bug behavior

Practical Use Cases

Use Case 1: Unit test for acf/update_value phone normalization

A sales contact field must always be saved in E.164 format.

  1. Register normalization filter in test bootstrap.
  2. Apply the filter with malformed phone input.
  3. Assert normalized value exactly matches expected format.
  4. Add one regression case for already normalized input.
CLI Inspection

Validate this state through code and CLI checks: Show CI test output with test_sales_phone_normalization passing.

tests/test-sales-phone-hook.php
<?php
class Test_Sales_Phone_Hook extends WP_UnitTestCase {
public function test_sales_phone_normalization() {
$result = apply_filters(
'acf/update_value/name=sales_phone',
'(65) 6700-8899',
101,
['name' => 'sales_phone']
);

$this->assertSame('+6567008899', $result);
}
}
?>
Expected output
OK (1 test, 1 assertion)

Use Case 2: Integration test for bidirectional relationship sync

Event saves should update linked speaker records through acf/save_post automation.

  1. Seed an event post and two speaker posts in test setup.
  2. Update event relationship field with selected speaker IDs.
  3. Trigger acf/save_post and fetch reverse field from each speaker.
  4. Assert event ID appears exactly once in reverse arrays.
CLI Inspection

Validate this state through code and CLI checks: Capture test report showing relation sync assertion counts.

tests/test-event-speaker-sync.php
<?php
class Test_Event_Speaker_Sync extends WP_UnitTestCase {
public function test_event_syncs_to_speaker_reverse_field() {
$event_id = self::factory()->post->create(['post_type' => 'event']);
$speaker_id = self::factory()->post->create(['post_type' => 'speaker']);

update_field('event_speakers', [$speaker_id], $event_id);
do_action('acf/save_post', $event_id);

$reverse = get_field('speaker_events', $speaker_id) ?: [];
$this->assertContains($event_id, $reverse);
}
}
?>
Expected output
OK (1 test, 2 assertions)

Common Mistakes

MistakeWhat HappensBetter Approach
Testing only return values, not side effectsSync and persistence bugs go undetectedAdd assertions for stored fields and related records
Sharing mutable fixtures across testsTests fail intermittently in CIIsolate fixtures per test method or reset state
Ignoring edge cases after bug fixesSame incidents recur in later releasesAdd regression tests immediately after every fix

Best Practices

  1. Keep hook callbacks pure where possible to simplify assertions.
  2. Use clear test method names that describe expected business behavior.
  3. Include edge-case tests for null, legacy, and malformed values.
  4. Run hook test suites on every pull request.
  5. Treat failing hook tests as release blockers for affected modules.

Hands-On Practice

  1. Write one unit test for an acf/update_value filter in your project.
  2. Add one integration test for acf/save_post side effects on related records.
  3. Create a regression test for a previously fixed ACF bug.

What's Next