Skip to main content
Version: 1.x
Testing · v1.x

Testing: Pest

Introduction​

Mantle's Testing Framework supports writing tests with Pest through the alleyinteractive/pest-plugin-wordpress package. Every feature of the testing framework is available in Pest tests: requests, factories, users, remote request fakes, assertions, and more. Pest tests using Mantle can be run with or without the rest of the framework.

use function Pest\PestPluginWordPress\fetchPost;

it( 'displays a single post', function () {
fetchPost( [ 'post_title' => 'Hello Pest' ] )
->assertOk()
->assertSee( 'Hello Pest' )
->assertQueryTrue( 'is_single', 'is_singular' );
} );
note

The plugin requires PHP 8.3+ and supports Pest 4 and Pest 5.

Installation​

Install the plugin via Composer:

composer require alleyinteractive/pest-plugin-wordpress --dev

Using the Mantle Framework​

On a Mantle application, run the pest:install command to create a tests/Pest.php file and an example test:

wp mantle pest:install

You can generate new Pest tests with the pest:test command. The name is relative to the tests/ directory:

wp mantle pest:test Feature/PostTest

Without the Mantle Framework​

Pest can be used on any WordPress plugin or theme through Mantle Testkit. If you don't have Pest set up already, create a tests folder and run pest --init:

./vendor/bin/pest --init

Open the tests/Pest.php file it created and bind your tests to the Testkit test case, then install WordPress:

use Mantle\Testkit\TestCase;

uses( TestCase::class )->in( __DIR__ );

\Mantle\Testing\install();

You can customize the installation with the Installation Manager instead of calling install() directly. For example, to install WordPress using SQLite:

use Mantle\Testkit\TestCase;

uses( TestCase::class )->in( __DIR__ );

\Mantle\Testing\manager()
->with_sqlite()
->install();

Running Tests​

Run your tests with the Pest binary:

./vendor/bin/pest

Writing Tests​

Pest binds each test closure to the test case configured in tests/Pest.php, so $this inside a test is a Mantle test case. Any method you would call in a class-based test works the same way in Pest:

it( 'creates a published post', function () {
$post = static::factory()->post->create_and_get();

$this->assertEquals( 'publish', $post->post_status );

$this->get( $post )->assertOk();
} );

The plugin also provides namespaced functions that wrap the most common methods, which lets you write tests without $this and pairs well with Pest's expectations. Import the functions you need with use function:

use function Pest\PestPluginWordPress\factory;

it( 'creates a post', function () {
$post = factory()->post->create_and_get();

expect( $post )
->toBeInstanceOf( WP_Post::class )
->post_status->toBe( 'publish' );
} );

Hooks and Shared Setup​

Pest's beforeEach() and afterEach() hooks are also bound to the test case, which makes them a good place for setup shared across the tests in a file:

use function Pest\PestPluginWordPress\factory;
use function Pest\PestPluginWordPress\get;

beforeEach( function () {
$this->category = factory()->category->create_and_get( [ 'name' => 'News' ] );
} );

it( 'loads the category archive', function () {
get( get_term_link( $this->category ) )
->assertOk()
->assertQueryTrue( 'is_archive', 'is_category' )
->assertQueriedObjectId( $this->category->term_id );
} );

You can also apply Mantle traits such as Refresh_Database to every test from tests/Pest.php:

use Mantle\Testing\Concerns\Refresh_Database;
use Mantle\Testkit\TestCase;

uses( TestCase::class, Refresh_Database::class )->in( __DIR__ );

Datasets​

Pest's datasets work with Mantle's request testing to run the same test against several inputs:

use function Pest\PestPluginWordPress\get;

it( 'loads core pages', function ( string $path ) {
get( $path )->assertOk();
} )->with( [
'homepage' => '/',
'feed' => '/feed/',
'sitemap' => '/wp-sitemap.xml',
] );

HTTP Requests​

The request functions mirror the HTTP testing methods and return a Test_Response with all of the available assertions:

use function Pest\PestPluginWordPress\factory;
use function Pest\PestPluginWordPress\get;

it( 'loads the homepage', function () {
get( '/' )
->assertOk()
->assertSee( get_bloginfo( 'name' ) );
} );

it( 'returns a 404 for a missing page', function () {
get( '/this-page-does-not-exist/' )->assertNotFound();
} );

it( 'returns a post from the REST API', function () {
$post_id = factory()->post->create();

get( rest_url( "wp/v2/posts/{$post_id}" ) )
->assertOk()
->assertJsonPath( 'id', $post_id );
} );

fetchPost() creates a post with the factory and requests its permalink in one call:

use function Pest\PestPluginWordPress\fetchPost;

it( 'renders the post title', function () {
fetchPost( [ 'post_title' => 'Example Title' ] )
->assertOk()
->assertElementExists( '//h1[contains(., "Example Title")]' );
} );

Use withHeader(), from(), and request() to build a request fluently before sending it:

use function Pest\PestPluginWordPress\from;
use function Pest\PestPluginWordPress\request;
use function Pest\PestPluginWordPress\withHeader;

it( 'sends a custom header', function () {
withHeader( 'X-Custom-Header', 'value' )
->get( '/' )
->assertOk();
} );

it( 'loads with a referrer', function () {
from( 'https://example.com/' )
->get( '/' )
->assertOk();
} );

it( 'requests JSON', function () {
request()
->with_headers( [ 'Accept' => 'application/json' ] )
->get( rest_url( 'wp/v2/posts' ) )
->assertOk()
->assertIsJson();
} );

Users and Authentication​

actingAs() authenticates as a user for the rest of the test. It accepts a user object, user ID, or a role name, which creates a new user with that role. See Users and Authentication for more information.

use function Pest\PestPluginWordPress\actingAs;
use function Pest\PestPluginWordPress\assertAuthenticated;
use function Pest\PestPluginWordPress\assertNotAuthenticated;
use function Pest\PestPluginWordPress\get;

it( 'starts as a guest', function () {
assertNotAuthenticated();
} );

it( 'allows editors into the admin', function () {
$user = actingAs( 'editor' );

assertAuthenticated( $user );

expect( current_user_can( 'edit_others_posts' ) )->toBeTrue();
} );

Remote Requests​

fakeRequest() fakes responses to remote requests made with the WP_Http API. It accepts the same arguments as $this->fake_request(), so you can fake a single URL, a set of URLs, or use a callback. See Remote Requests for all of the ways to build a response.

use function Pest\PestPluginWordPress\fakeRequest;
use function Pest\PestPluginWordPress\preventStrayRequests;

beforeEach( function () {
preventStrayRequests();
} );

it( 'fetches the latest release', function () {
fakeRequest( 'https://api.github.com/repos/*' )
->with_json( [ 'tag_name' => 'v1.0.0' ] );

$response = wp_remote_get( 'https://api.github.com/repos/alleyinteractive/mantle/releases/latest' );

expect( json_decode( wp_remote_retrieve_body( $response ), true ) )
->tag_name->toBe( 'v1.0.0' );

$this->assertRequestSent( 'https://api.github.com/repos/alleyinteractive/mantle/releases/latest' );
} );

it( 'fakes multiple endpoints', function () {
fakeRequest( [
'https://github.com/*' => mock_http_response()->with_body( 'github' ),
'https://example.com/*' => mock_http_response()->with_status( 404 ),
] );

expect( wp_remote_retrieve_response_code( wp_remote_get( 'https://example.com/missing' ) ) )
->toBe( 404 );
} );

preventStrayRequests() causes any remote request that isn't faked to fail, and allowStrayRequests() turns that back off. See preventing stray requests for more information.

Available Functions​

All functions live in the Pest\PestPluginWordPress namespace.

FunctionEquivalent MethodDescription
get( $uri, $headers )$this->get()Make a GET request.
post( $uri, $data, $headers )$this->post()Make a POST request.
put( $uri, $data, $headers )$this->put()Make a PUT request.
patch( $uri, $data, $headers )$this->patch()Make a PATCH request.
delete( $uri, $data, $headers )$this->delete()Make a DELETE request.
options( $uri, $data, $headers )$this->options()Make an OPTIONS request.
head( $uri, $headers )$this->head()Make a HEAD request.
fetchPost( $arguments )Create a post and request its permalink.
request()$this->with_headers( [] )Start a fluent request.
withHeader( $name, $value )$this->with_header()Start a request with a header.
from( $url )$this->with_header( 'referer', $url )Start a request with a referrer.
factory()static::factory()Get the factory container.
actingAs( $user )$this->acting_as()Authenticate as a user.
assertAuthenticated( $user )$this->assertAuthenticated()Assert a user is authenticated.
assertNotAuthenticated()$this->assertNotAuthenticated()Assert no user is authenticated.
fakeRequest( $url_or_callback, $response, $method )$this->fake_request()Fake a remote request.
preventStrayRequests( $response )$this->prevent_stray_requests()Fail any remote request that isn't faked.
allowStrayRequests()$this->allow_stray_requests()Allow remote requests that aren't faked.

For anything not covered by a function, use $this inside the test closure.