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.

Discover more from JuanMa Codes

Subscribe now to keep reading and get access to the full archive.

Continue reading