tl;dr: is there a way to make changes to WordPress options during a PHPUnit test suite execution, and have the changes reverted to the WordPress defaults before the next test suite is executed, without writing a custom teardown function?

I'm testing my plugin which provides a function to add and remove some roles all at once. The users of my plugin should only have to run this function once, but they can choose not to run it at all, so I set up two test suites (using WP-CLI's scaffolding) to test the situation both before and after converting roles. Since the role conversion operation makes changes to the database, I separated the tests on the converted roles into their own PHPUnit group, and excluded this group from running by default. To test the role changing function, I therefore have to call PHPUnit with the --group role-permissions flag. I can also run the tests on the default WordPress roles by calling PHPUnit without a group. I run into a problem when I run the tests one after the other.

First, if I run the default tests...

$> phpunit
[passes]

...they initially pass. If I then run the special roles tests...

$> phpunit --group role-permissions
[passes]

...then they also pass. But if I run the default tests again after this...

$> phpunit
[fails]

...they no longer pass. I found that this is because the options changed by the role-permissions tests are still present in the test database before the default ones are run again. The only way I can get the default tests to pass again is to regenerate the default WordPress test database.

To convert the roles so I can run the role-permissions tests, I have some code in wpSetUpBeforeClass. This runs only once per PHPUnit execution, before tests are run, so this seems like the right place to put the code. However, clearly the test scaffolding code does not restore the default wptests_options database table after each run.

Is there a way to restore the default options in the database after my special tests have run, or run my role-permissions tests in their own database, or some other way to prevent the failures I get?

For reference, the stripped down versions of the relevant files are shown below:

tests/test-default-roles.php:

/**
 * // no special group
 */
class OtherTests extends WP_UnitTestCase {    
    public function test_default_roles() {
        // test stuff with the default WordPress roles
    }
}

tests/test-new-roles.php:

/**
 * @group role-permissions
 */
class RoleTests extends WP_UnitTestCase {
    /**
     * Convert roles before running any test
     * (this changes the wp_settings table)
     */
    public static function wpSetUpBeforeClass( $factory ) {
        // convert roles
        global $my_tool;
        $my_tool->convert_roles();
    }

    public function test_new_roles() {
        // test some stuff to do with the converted roles
    }
}

phpunit.xml

...
<testsuites>
    <testsuite>
        <directory prefix="test-" suffix=".php">./tests/</directory>
    </testsuite>
</testsuites>
<groups>
    <exclude>
        <!-- exclude role conversion tests from running by default -->
        <group>role-permissions</group>
    </exclude>
</groups>
...
  • 1
    not an answer... if the initial state is important, than you are writing bad tests or bad code. Especially if you are writing a plugin or theme you should not assume anything about the state in which they are being activated. As to why there is no cleanup in the core tests, the reason is that each file will start with a clean DB, the DB use for testing is a "virtual" one and is not saved to the disk – Mark Kaplun May 6 at 17:44
  • I guess they're more like integration tests, and I'm (mis?)using the WordPress unit test framework for it. If what you say about each file starting with a clean database is true, then that's not what I am seeing when I run my tests sequentially... – Sean May 6 at 18:38
  • yeh I probably do not remember correctly, but if you inherit the core UT class of wordpress it should setup a clean WP (frop tables). IIRC the times in which an actual write might happen is if you have some error in the test that breaks phpunit itself. again not an answer. been a while since I have done wordpress UT. – Mark Kaplun May 7 at 1:21
  • Thanks for your help - I took your tip and looked for the WordPress core clean up function and adapted it for my case. – Sean May 7 at 7:38
up vote 2 down vote accepted

tl;dr: is there a way to make changes to WordPress options during a PHPUnit test suite execution, and have the changes reverted to the WordPress defaults before the next test suite is executed, without writing a custom teardown function?

Yes, and no.

No, you cannot use the code you currently have and expect this result. The test suite does use transactions and automatically roll back the database after each test. But you are making your changes in wpSetUpBeforeClass(), which runs before the transaction begins. Anything you do in wpSetUpBeforeClass() you have to clean up yourself in wpTearDownAfterClass(). This is by design.

However, you don't have to use wpSetUpBeforeClass(). You could just place your code in setUp() instead. By calling parent::setUp() before making your changes, the database transaction will already have started and so your changes will be rolled back automatically after each test is complete.

  • Ah, that's great to know, thanks! I'll probably do that. Do you know if it's worth keeping the teardown code I wrote in my own answer anyway, or can I always assume that the database will be rolled back? Some MySQL engines don't support transactions, so is there a possibility the test might be run on such a database, or will the WP CLI scaffolding prevent this? – Sean May 7 at 13:30
  • I think that the test suite forces InnoDB to be used for the tables created for the tests, so it is a safe assumption that transactions will be possible. This is a fundamental part of WordPress's test suite design. – J.D. May 7 at 17:57

After Mark Kaplun's comment, I did a little digging in the WordPress test source code and found the _delete_all_data function which is run after each test:

function _delete_all_data() {
    global $wpdb;
    foreach ( array(
        $wpdb->posts,
        $wpdb->postmeta,
        $wpdb->comments,
        $wpdb->commentmeta,
        $wpdb->term_relationships,
        $wpdb->termmeta,
    ) as $table ) {
        $wpdb->query( "DELETE FROM {$table}" );
    }
    foreach ( array(
        $wpdb->terms,
        $wpdb->term_taxonomy,
    ) as $table ) {
        $wpdb->query( "DELETE FROM {$table} WHERE term_id != 1" );
    }
    $wpdb->query( "UPDATE {$wpdb->term_taxonomy} SET count = 0" );
    $wpdb->query( "DELETE FROM {$wpdb->users} WHERE ID != 1" );
    $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE user_id != 1" );
}

As you can see, this function reverts (technically deletes) only posts, comments, terms and users are deleted. Options and other site configuration stuff are not reverted.

The way I found to fix my problem is to back up the roles option before changing it, and revert it afterwards. This isn't the solution I would have preferred, but it seems to be the best one available. First, I define the class setup and teardown methods that run once before and after all tests are run:

public static function wpSetUpBeforeClass( $factory ) {
    self::convert_roles();
}

public static function wpTearDownAfterClass() {
    self::reset_roles();
}

Then I define the functions to create the new roles and to reset them to defaults before. For these, I need some static variables defined in the class.

/**
 * Convert user roles for the tests in this class.
 * 
 * This only works when the current blog is the one being tested.
 */
private static function convert_roles() {
    global $wpdb, $my_tool;

    // role settings name in options table
    self::$role_key = $wpdb->get_blog_prefix( get_current_blog_id() ) . 'user_roles';

    // copy current roles
    self::$default_roles = get_option( self::$role_key );

    // convert roles
    $my_tool->convert_roles();
}

/**
 * Reset changes made to roles
 */
private static function reset_roles() {
    update_option( self::$role_key, self::$default_roles );

    // refresh loaded roles
    self::flush_roles();
}

/**
 * From WordPress core's user/capabilities.php tests
 */
private static function flush_roles() {
    // we want to make sure we're testing against the db, not just in-memory data
    // this will flush everything and reload it from the db
    unset( $GLOBALS['wp_user_roles'] );
    global $wp_roles;
    $wp_roles = new WP_Roles();
}

The static variables in the class can be defined as follows:

protected static $role_key;
protected static $default_roles;

Now my tests pass no matter what order they are called.

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.