سؤال

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>
...
هل كانت مفيدة؟

المحلول

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.

نصائح أخرى

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.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى wordpress.stackexchange
scroll top