Debugging WordPress Tests: database transactions

internet technology computer display

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 here

I 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

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 ROLLBACK

With START TRANSACTION, autocommit is disabled for your session until you end the transaction.

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., default wp_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 in wpTearDownAfterClass(). (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:

  1. Confirm autocommit state in the test session (e.g., from a breakpoint or wp shell attached to the test process):
    SELECT @@session.autocommit; → it should be 0 during the test (transaction open).
  2. 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.)
  1. 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 in wpTearDownAfterClass(). 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 transaction

And 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 ran COMMIT (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

  1. Tests run inside a transaction.
  2. Other sessions can’t see uncommitted inserts.
  3. Run COMMIT (or SET autocommit=1) in the Debug Console.
  4. 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.

Resources

Leave a Reply

Navigation

About

Writing on the Wall is a newsletter for freelance writers seeking inspiration, advice, and support on their creative journey.