When I was debugging WordPress integration tests with Xdebug, I ran into something that really confused me.
I used the WordPress test factory to create an administrator user:
$admin_id = self::factory()->user->create( [ 'role' => 'administrator' ] );
wp_set_current_user( $admin_id ); // <-- breakpoint hereI paused on that line, opened MySQL in another terminal, and ran:
USE tests-wordpress;
SELECT COUNT(*) FROM wp_users;I expected to see two users… but it still said there was only one. 🤔
Table of Contents
- What’s actually happening
- MySQL transactions: a practical primer
- The quick fix
- Handy extras
- TL;DR
- Resources
What’s actually happening
Turns out nothing was wrong with the factory. The issue was transactions.
- The test is running inside a transaction (
@@session.autocommit = 0). - That means the insert isn’t committed yet.
- Your PHP process can see the user, but other MySQL sessions can’t.
So the user exists—but it’s invisible to anything outside that transaction.
MySQL transactions: a practical primer
Understanding how MySQL transactions work and how they behave inside WordPress’ PHPUnit test suite is crucial to debug Unit Tests with confidence. Here’s a practical primer:
1) MySQL transactions in a nutshell
- What a transaction is: a group of SQL statements that succeed or fail as one unit. You explicitly start one, then either COMMIT (make changes permanent) or ROLLBACK (undo them).
- Autocommit is on by default: every statement commits immediately unless you’ve started a transaction. Check with
SELECT @@autocommit;(1 means on). - How to use them:
START TRANSACTION; -- or BEGIN
-- your INSERT/UPDATE/DELETE...
COMMIT; -- or ROLLBACKWith START TRANSACTION, autocommit is disabled for your session until you end the transaction.
- Each session is isolated: uncommitted changes are only visible to the session that made them; other sessions can’t see them until you COMMIT. (MySQL’s isolation defaults to REPEATABLE READ.)
- Beware implicit commits: certain statements (notably DDL like
CREATE TABLE,ALTER TABLE,DROP TABLE, plusTRUNCATE, etc.) automatically end any open transaction before they run. This can surprise you during tests or debugging.
2) How WordPress’ PHPUnit tests use transactions
WordPress’ core PHP test suite uses a transaction-per-test model:
- When you run
phpunit, the suite bootstraps a clean WP install in a dedicated test DB. That baseline data (e.g., defaultwp_options) persists across the whole run. - Before each test method, the suite starts a MySQL transaction and disables autocommit for that session.
- After each test, it issues a ROLLBACK, discarding all DB changes made during that test. This keeps tests isolated and fast.
A few important nuances:
- Shared/class fixtures:
wpSetUpBeforeClass()runs outside the per-test transaction window. Anything you create there won’t be auto-rolled back; you’re expected to clean it up inwpTearDownAfterClass(). (WordPress Stack Exchange discussion, WordPress testing guide) - DDL inside tests: If you create/drop tables in a test, those DDL statements can implicitly commit and break isolation. Prefer avoiding DDL in individual tests, or do it in class setup/teardown with explicit cleanup.
3) Debugging: “Why can’t my second session see the data I just created in a test?”
Because the test runner’s session hasn’t committed — it’s sitting inside an open transaction. From another MySQL session you’ll only see committed rows.
Quick checks & tactics:
- Confirm autocommit state in the test session (e.g., from a breakpoint or
wp shellattached to the test process):SELECT @@session.autocommit;→ it should be0during the test (transaction open). - To observe fixtures externally (for debugging only):
- From the test session, issue
COMMIT;while paused, then query from your external session. - Remember: you’ve now defeated the test harness’s rollback for this test; those rows will persist until the DB is reset. Use this sparingly and only in the throwaway test database. (Next test will open a fresh transaction, but your committed data remains.)
- Avoid implicit commits while debugging: don’t run DDL from inside the paused test unless you mean to end the transaction.
4) Practical do’s and don’ts in WP tests
- Do create test fixtures (users, posts, terms) inside each test with the factories; they’ll be rolled back automatically.
- Do put expensive, reusable fixtures in
wpSetUpBeforeClass()and clean them inwpTearDownAfterClass(). Understand they live outside per-test transactions. - Don’t run DDL in a test unless absolutely necessary; it can implicitly commit and leak state.
- Don’t expect another client/session (your SQL console) to see uncommitted data from a running test — that’s how MySQL transaction isolation is supposed to work.
The quick fix
Here’s the easy way I deal with this when I want to inspect data from MySQL while paused in Xdebug step debugging.
1. Check if you’re in a transaction
From your IDE’s Debug Console (or PhpStorm’s Evaluate Expression):
$GLOBALS['wpdb']->get_var('SELECT @@session.autocommit');
// 0 means you’re in a transactionAnd confirm the row exists in your paused process:
$GLOBALS['wpdb']->get_var( 'SELECT COUNT(*) FROM ' . $GLOBALS['wpdb']->users ); 2. Commit so MySQL can see it
Still in the Debug Console, run one of these:
$GLOBALS['wpdb']->query('COMMIT');
// or
$GLOBALS['wpdb']->query('SET SESSION autocommit=1');This commits the insert and makes it visible to other sessions.
3. Check from your MySQL client
Back in the terminal:
USE tests-wordpress;
SELECT COUNT(*) FROM wp_users; -- now 2 🎉And just like that, you can see the new row.
Handy extras
- Make sure you’re looking at the same DB:
SELECT DATABASE();From the IDE console:
$GLOBALS['wpdb']->get_var('SELECT DATABASE()');- Clean up after yourself (only if you committed):
Normally you don’t need this—WordPress PHPUnit tests wrap everything in a transaction and roll it back automatically. But if you manually ranCOMMIT(so you could see rows from another session), that data will stick. In that case:
wp_delete_user( $admin_id );Or, if you haven’t committed yet and just want to discard:
$GLOBALS['wpdb']->query('ROLLBACK');- Built-in debug shortcut (collect info in your test):
Instead of switching to MySQL or committing, pre-compute a few handy variables in your test so you can inspect them at the breakpoint:
$admin_id = self::factory()->user->create( [ 'role' => 'administrator' ] );
global $wpdb;
$cnt = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" );
$u = $wpdb->get_row( "SELECT ID, user_login FROM {$wpdb->users} WHERE ID = {$admin_id}", ARRAY_A );
$meta = $wpdb->get_results( "SELECT meta_key, meta_value FROM {$wpdb->usermeta} WHERE user_id = {$admin_id} LIMIT 10", ARRAY_A );
$auto = $wpdb->get_var( 'SELECT @@session.autocommit' );
$db = $wpdb->get_var( 'SELECT DATABASE()' );
$host = $wpdb->get_var( 'SELECT @@hostname' );
$port = $wpdb->get_var( 'SELECT @@port' );
wp_set_current_user( $admin_id );Now, at pause time you can quickly inspect:$cnt (users count), $u (new user), $meta (sample meta), $auto (in-transaction flag), and connection details ($db, $host, $port)—all without leaving your IDE.
TL;DR
- Tests run inside a transaction.
- Other sessions can’t see uncommitted inserts.
- Run
COMMIT(orSET autocommit=1) in the Debug Console. - Now your external MySQL client shows the changes.
That’s the loop I went through. Hope this is also useful for you if you also get stuck here sometime.

Leave a Reply