Four WordPress integration testing multisite pieces

Multisite-ish.

Better together

This post follows a previous introductory one and extends some basic concepts of WordPress integration testing done with either the PhpUnit based WordPress Core automated testing suite or wp-browser.
As in the previous post I will point out differences between the two if any.

Multisite integration testing

WordPress can be installed in one of two similar yet distinct modes: single site and multisite installation.
The first is the most widely used one while the second is the one that allows the same WordPress code base, themes and plugins to power multiple distinct sites each with its own posts, settings and options.
The WordPress codex explains some caveats and important points to keep in mind.
By default both the Core suite and wp-browser will run the tests in the context of a single site installation, but it’s easy to instruct them to run the tests in the context of a multisite installation.
The Core suite is controlled using the wp-tests-config.php file (see a sample) setting the WP_TESTS_MULTISITE constant to true; the sample configuration file has that line commented out, it’s just a matter of uncommenting it:

define( 'WP_TESTS_MULTISITE', true );

wp-browser delegates integration testing to the WPLoader module and its configuration accepts a multisite parameter to control the installation context:

 modules:
      enabled:
          - WPLoader
      config:
          WPLoader:
              multisite: true
              wpRootFolder: "/Users/User/www/mu-wordpress"
              dbName: "mu-wpress-tests"
              dbHost: "localhost"
              dbUser: "root"
              dbPassword: "root"
              tablePrefix: "wptests_"
              domain: "wordpress.dev"
              adminEmail: "admin@wordpress.dev"
              title: "Test Blog"
              plugins: ['my-plugin/my-plugin.php']
              activatePlugins: ['my-plugin/my-plugin.php']

Writing integration tests for a multisite installation is not different from what’s done for single site installations:

wpcept generate:wpunit integration Mu

Who’s who

The default user, when running integration tests in a single site installation, is the visitor: the user with and ID of 0.
What’s different in a multisite installation?

public function test_multisite_users() {
    $this->assertNotFalse( get_user_by( 'ID', 1 ) );
    $this->assertCount( 1, get_users() );
    $this->assertEquals( 0, get_current_user_id() );
}

This time too the current user is the visitor and the database contains one user, the one with an ID of 1. So who’s this 1 user?

public function test_user_one() {
    $this->assertTrue( is_super_admin( 1 ) );
    $this->assertTrue( user_can( 1, 'create_sites' ) );
}

user-1

User ‘1’ is the super-administrator or “he who can create sites”.
It’s important to keep in mind that while the super-admin will always exist when the tests start he’s not the current user.

Creating blogs

In my previous post I’ve given a quick look at factories and how those can make scaffolding complex fixtures easy; I’ve indicated that the network and blog factories would be available in multisite testing mode only so: what can they do?

public function test_blogs_factory() {
    $blogId = $this->factory()->blog->create( [ 'domain' => 'foo', 'path' => '/', 'title' => 'Blog 1' ] );

    $this->assertEquals( $blogId, domain_exists( 'foo', '/' ) );

    switch_to_blog( $blogId );

    $this->assertEquals( 0, get_current_user_id() );
    $this->assertFalse( current_user_can( 'create_sites' ) );

    $superadminBlogs = get_blogs_of_user( 1 );

    $this->assertNotContains( $blogId, wp_list_pluck( $superadminBlogs, 'userblog_id' ) );

    $this->assertEmpty( get_users( [ 'blog_id' => $blogId ] ) );
}

blogs-1

Factories are so powerful that blogs can be created on the fly but, more importantly, the current user, still 0, is not taken into account: visitors are not normally allowed to create blogs.
So much so that the new blog has no users assigned to it. The power of tests is that, for the most part, user_can() kind of checks are skipped calling underlying functions and methods directly and that allows for some daring combination of fixtures.
Joining the user and the blog factory one can assign a new blog a user as administrator directly:

public function test_users_and_blogs() {
    $this->assertEquals( 1, get_current_blog_id() );

    $userId = $this->factory()->user->create();
    $blogId = $this->factory()->blog->create( [ 'user_id' => $userId, 'domain' => 'foo', 'path' => '/' ] );

    $this->assertNotEquals( 1, $blogId );

    wp_set_current_user( $userId );

    $this->assertTrue( current_user_can_for_blog($blogId, 'edit_dashboard' ) );
    $this->assertFalse( current_user_can_for_blog( 1, 'edit_dashboard' ) );
}

blogs-2

Subdomains and sub folders

Anyone practical enough with multisite installation knows that sub-sites can be created in one of two ways:

  • sub-domain - foo.wordpress.dev
  • sub-folder - wordpress.dev/foo

While the difference is huge at the site server level it is menial at the integration testing level:

public function test_subdomain_and_subfolder_blogs(  ) {
    $subdomainId = $this->factory()->blog->create( [ 'domain' => 'foo', 'path' => '/' ] );
    $subfolderId = $this->factory()->blog->create( [ 'domain' => '', 'path' => '/foo' ] );

    $this->assertEquals( $subdomainId, domain_exists( 'foo', '/' ) );
    $this->assertEquals( $subfolderId, domain_exists( '', '/foo' ) );
}

blogs-3

I’ve used the same subsite name on purpose, foo, to show that the two are completely different.

New super administrators

Can new super administrators, beyond the default one, be created?

public function test_superadmin_creation() {
    $user = $this->factory()->user->create();

    grant_super_admin( $user );

    $this->assertNotEquals( 1, $user );
    $this->assertTrue( is_super_admin( $user ) );
}

superadmin

Fabricating networks

The last frontier of factories is generating networks and it’s a strong testimony to how powerful factories can be.

public function test_network_creation() {
    $this->assertEquals( 1, get_current_network_id() );

    $network = $this->factory()->network->create();

    $this->assertNotEquals( 1, $network );

    $blog = $this->factory()->blog->create( [ 'site_id' => $network ] );

    switch_to_blog( $blog );

    $this->assertEquals($blog,get_current_blog_id());

    $this->assertNotEquals( $network, get_current_network_id() );
}

mu-1

Many things

I will use the blog and network factory to demonstrate another powerful feature of the factories: creating many things.

public function test_many_creation(  ) {
    $sites = $this->factory()->network->create_many(3);

    $this->assertCount(3,$sites);

    $blogs = call_user_func_array('array_merge',array_map(function($site){
            return $this->factory()->blog->create_many(3, [ 'site_id' => $site ]);
        }, $sites));

    $this->assertCount(9,$blogs);

    $posts = call_user_func_array('array_merge',array_map(function($blog){
        switch_to_blog($blog);
        return $this->factory()->post->create_many(3);
    },$blogs));

    $this->assertCount(27,$posts);
}

many