Foundations

Why Testing AI Abilities Is Different From Testing Ordinary PHP

⏱ 13 min

Here’s a claim worth being precise about before this course goes any further: an ability is not hard to test. wp_register_ability() registers a namespaced name, a schema, a permission_callback, and an execute_callback, all plain PHP, all testable with ordinary PHPUnit exactly the way you’d test any other WordPress hook-driven code. Nothing about the Abilities API itself resists testing. What resists testing is a specific, common design decision: an execute_callback that reaches out to an LLM through the PHP AI Client SDK before returning. That one line changes what “correct” even means for the rest of the function, and this lesson draws the line between the part that stays ordinary and the part that doesn’t.

What you'll learn in this lesson
Why abilities without an AI call are just PHP
Registration, permission, and execution you can assert on exactly, no different from testing any other plugin function.
What actually breaks when execute_callback calls an LLM
Non-deterministic output and an external dependency, two distinct problems, not one.
Why there is no SDK mock for wp_ai_client_prompt()
The AI Client SDK has no built-in test double, so "just mock the AI call" isn't an available option.
The four testable layers of an AI-calling ability
Which ones take exact assertions and which one needs a different kind of test entirely.
Prerequisites

A registered ability and a working MCP server (Course 2). Familiarity with wp_register_ability(), permission_callback, and execute_callback from Course 1, and with wp_ai_client_prompt() from the PHP AI Client SDK course. No PHPUnit experience assumed yet, that starts in the next lesson.

Step 1: Most of an ability is already ordinary PHP

Take an ability with no AI dependency at all, one that lists open support tickets by querying a custom post type. Its permission_callback checks a capability. Its execute_callback runs a WP_Query and shapes the result. Both are deterministic: same input, same WordPress state, same output, every single time. A PHPUnit test against either one is exactly as easy as testing any other plugin function that reads or writes WordPress data, assertTrue(), assertSame(), assertCount(), done. The next lesson sets up the test suite, and the lesson after that writes exactly these tests. None of that work is complicated by anything AI-related, because there’s nothing AI-related in it yet.

Step 2: Naming the actual problem precisely

The complication starts the moment an execute_callback calls wp_ai_client_prompt()->generate_text() before returning, the pattern the PHP AI Client SDK course builds as a summarize-post ability. That single call introduces two separate problems, and it matters that they’re separate, because they need two different fixes:

Two distinct problems, not one
1
Non-determinism
The same prompt can produce different, differently-worded output on different calls. assertSame( $expected_text, $result ) is the wrong tool for grading text like this, it will fail on correct output as often as on wrong output.
2
An external, costly dependency
A real API call has latency, a real cost, and a real chance of failing for reasons that have nothing to do with your code (a rate limit, a provider outage). A fast unit test suite that runs on every commit shouldn't be making one.

Both problems live inside the same function call, but they don’t share a fix. Fixing non-determinism means changing what you assert on and how you score it, not changing whether the call happens. Fixing the external dependency means changing whether the real call happens in a test at all, replacing it with something local and fast. Lessons 4 and 5 build one fix each.

Step 3: Why “just mock the AI call” isn’t sitting there waiting for you

If you’ve used WP_Mock or Brain\Monkey to stub a WordPress core function in a unit test, the instinct here is reasonable: stub wp_ai_client_prompt() the same way. It doesn’t work as cleanly as it sounds, for a reason worth being explicit about. The PHP AI Client SDK has no built-in test double for this call, no shipped fake provider, no documented “return this canned response instead of calling out” mode. generate_text() is the end of a fluent builder chain (with_text(), using_system_instruction(), using_temperature(), and so on), and stubbing a fluent chain with a general-purpose function-mocking library means stubbing every method in the chain and keeping all of them in sync with however your production code actually calls it, which is exactly the kind of brittle setup that breaks the moment someone adds one more builder call.

The direction matters, and it's easy to get backwards

It’s tempting to look for “how do I mock an LLM” guidance the same way you’d look for mocking any other external API, but the direction here is the opposite of what most people expect. The MCP Adapter’s own abilities have no LLM dependency at all, an external AI client calls into WordPress, WordPress doesn’t call out to one. The direction that needs mocking is the one this lesson is about: your own execute_callback calling out to AI, the pattern from the PHP AI Client SDK course, not anything inside the MCP Adapter itself.

Step 4: The real fix is a design pattern, not a mocking library

Since the SDK gives you nothing to mock, the fix has to happen one layer up, in how you write the execute_callback in the first place. Wrap the part that calls wp_ai_client_prompt() behind a small interface of your own, one method, a plain string or WP_Error in, a plain string or WP_Error out. Your production code implements that interface by calling the real SDK. Your tests substitute a different implementation that returns a canned value instantly, no network call, no API key, no non-determinism, because you’re testing your own contract, not the SDK’s fluent chain. This is ordinary dependency injection, the same technique any well-tested PHP codebase uses for a database connection or an HTTP client, applied to the one place in an ability where it’s actually needed. Lesson 4 builds it in full.

Step 5: The four layers, and how each one gets tested

What each layer of an AI-calling ability needs
LayerDeterministic?How it’s tested
Registration and schema shapeYesExact PHPUnit assertions, Lesson 3.
permission_callbackYesExact PHPUnit assertions across real user roles, Lesson 3.
execute_callback’s own logic (validation, error handling, shaping the result)Yes, once the AI call is injectableExact PHPUnit assertions against a fake dependency, Lesson 4.
The actual AI-generated outputNoEvals scored against a rubric, not exact-match, Lesson 5.

That last row is the one piece of an AI-calling ability that genuinely can’t be tested the way the other three are, and trying to force it into an exact assertion is the single most common mistake teams make here: either skipping testing the AI output entirely, or writing a brittle assertStringContainsString() test against one particular phrase that happens to break the next time the model or the prompt changes slightly. Lesson 5 gives that layer its own approach.

Test it: sort your own abilities into these four layers

Before writing a single test, list every ability your plugin registers and mark which of the four rows in Step 5’s table actually applies to each one. Most abilities will only have the first three rows, no AI call at all. A permission_callback that checks current_user_can() and an execute_callback that runs get_posts() never touch row four. Only abilities matching the PHP AI Client SDK course’s summarize-post pattern need it. Knowing which of your abilities need which kind of test, before you write any of them, is the entire point of this lesson.

Recap

Abilities are plain PHP, and testing registration, permission_callback, and an execute_callback with no AI dependency is ordinary PHPUnit work, nothing about the Abilities API changes that. The real complication is narrower than it sounds: an execute_callback that calls wp_ai_client_prompt() introduces non-determinism and an external dependency, two separate problems needing two separate fixes. There is no built-in SDK mock for the AI call, so the fix is architectural, wrap the AI-calling logic behind your own small interface so it’s injectable in tests, and grade the actual generated output with evals rather than exact assertions. The next two lessons build the ordinary PHPUnit half, Lessons 4 and 5 build the two AI-specific fixes.

Resources & further reading

Setting Up PHPUnit for Ability Testing →