Doing a complete test of our code is very important whenever we make some code changes or release a new version to make sure new functionality doesn’t break the existing ones. Doing it all manually is the very hard part rather than developing it. So how about automating it? Let’s see how to automate our unit testing of WordPress plugins using PHPUnit.

Most of the WordPress plugins are not well tested and maybe not even have unit tests. That’s because;

  1. Developers don’t have much idea about the plugin testing
  2. They think that it will slow down their work
  3. Don’t understand the benefit of the well-written tests.

Automated Testing

PHPUnit is a unit testing framework for the PHP programming language. It is an instance of the xUnit architecture for unit testing frameworks that originated with SUnit and became popular with JUnit. PHPUnit was created by Sebastian Bergmann and its development is hosted on GitHub.

Wikipedia

Benefits

Writing a well written unit test helps in many ways.

  1. It ensures the software quality with every line of change
  2. Reduce the testing effort when the software grows
  3. Helps to find bugs early in the development phase
  4. Helps to debug the issue when something breaks
  5. Gives confident on the written code

Unit testing a WordPress plugin

Let’s take a real-world example of a WordPress plugin which displays a random quote at the end of every post. For the purpose of this post, we will get the quotes from a predefined set which is stored in code.

Complete source code can be found here.

We have 2 very simple class in this plugin class-quotes.php and random-quote-wp.php.

class-quotes.php will manage the quotes. For the purpose of this article, I have hardcoded 5 quotes in this class, but we can integrate any API’s or have a custom post type to store the quotes.

<?php
/**
* Quotes class
*/

class Quotes {
	/**
	* Quotes
	*/
	private $quotes = array(
		array(
			'quote' => 'The greatest glory in living lies not in never falling, but in rising every time we fall.',
			'by' => 'Nelson Mandela',
		),
		array(
			'quote' => 'The way to get started is to quit talking and begin doing.',
			'by' => 'Walt Disney',
		),
		array(
			'quote' => 'Your time is limited, so don\'t waste it living someone else\'s life. Don\'t be trapped by dogma – which is living with the results of other people\'s thinking.',
			'by' => 'Steve Jobs',
		),
		array(
			'quote' => 'There is noting like good thing or bad thing. It\'s all about majorities.',
			'by' => 'Dhanendran Rajagopal',
		),
		array(
			'quote' => 'Life is what happens when you\'re busy making other plans.',
			'by' => 'John Lennon',
		),
	);

	/**
	* Return a random quote.
	*
	* @return array
	*/
	public function get_quote() {
		return $this->quotes[ rand(0, 4) ];
	}
}

random-quote-wp.php will append the quote to the post body for the frontend.

<?php
require_once dirname(__FILE__) . '/class-quotes.php';

class Random_Quote_WP {

	/**
	* Hook into `the_content` action to append quote.
	*/
	public function init() {
		add_filter( 'the_content', array( $this, 'add_quote' ) );
	}

	/**
	* Prepare a templste for a quote.
	*
	* @return string
	*/
	public function get_quote_template() {

		$quote_object = new Quotes();

		$quote = $quote_object->get_quote();

		$template = sprintf( '<br/>
			<p>
				<b>Quote of the day</b>
				<blockquote> %s - %s</blockquote>
			</p>',
			$quote['quote'],
			$quote['by'] );

		return $template;
	}

	/**
	* Append a randon quote to post content.
	*
	* @return string
	*/
	public function add_quote( $content ) {

		return $content . $this->get_quote_template();
	}

}

( new Random_Quote_WP() )->init();

Our base code is ready and now we need to install PHPUnit for us to write tests for our plugin.

Installing PHPUnit

PHPUnit can be installed in two ways either with Phar file or composer. Run either one of the below command to install the PHPUnit in your local machine.

➜ wget https://phar.phpunit.de/phpunit-7.0.phar
➜ chmod +x phpunit-7.0.phar
➜ ./phpunit-7.0.phar --version
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

or

➜ composer require --dev phpunit/phpunit ^7.0
➜ ./vendor/bin/phpunit --version
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

We are specifically installing PHPUnit 7.0 version because that’s what WordPress supports as of WordPress 5.2 version.

Installing WP-CLI

To install WP-CLI, run the following commands.

➜ curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
➜ chmod +x wp-cli.phar
➜ sudo mv wp-cli.phar /usr/local/bin/wp

Run wp --info to confirm its installation.

Having installed PHPUnit and WP-CLI, we will use the WP-CLI to set up the unit test for the plugin.

Setup Unit test for plugin

WP-CLI provides a set of commands to create the base files needed to write and test the plugin code. Run below code from the root of your application.

➜ wp scaffold plugin-tests random-quote-wp

This will generate few directories, files and config files. Below is the file structure after the setup.

|-bin/
|----install-wp-tests.sh
|-tests/
|----bootstrap.php
|----test-sample.php
|-vendor/
|-phpcs.xml.dist
|-.travis.yml
|-class-quotes.php
|-composer.json
|-phpunit.xml.dist
|-random-quote-wp.php

Now we need to prepare the test DB to test our plugin code. PHPUnit will not use the application DB for testing purpose and it will use a separate DB for this. To create this new test DB, we need run the below command.

➜ bin/install-wp-tests.sh wordpress_test root 'secretPassword' 127.0.0.1 latest

This will create a new DB called wordpress_test. Now you can run the existing sample test to check.

➜ ./vendor/bin/phpunit

This will execute the sample test which is been added by the WP-CLI.

Installing…
Running as single site… To run multisite, use -c tests/phpunit/multisite.xml
Not running ajax tests. To execute these, use --group ajax.
Not running ms-files tests. To execute these, use --group ms-files.
Not running external-http tests. To execute these, use --group external-http.
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

.                       1 / 1 (100%)

Time: 1.24 seconds, Memory: 28.00 MB

OK (1 test, 1 assertion)

Writing tests for the plugin

Now everything in place for us to write tests for our plugin. Lets create a file named test-random-quote-wp.php in the tests directory along side the test-sample.php.

<?php
/**
 * Class Test_Random_Quote_WP
 *
 * @package Random_Quote_Wp
 */

/**
 * Tests for Random_Quote_Wp class
 */
class Test_Random_Quote_WP extends WP_UnitTestCase {

	/**
	* Holds post id.
	*
	* @var int
	*/
	private $post_id;

	/**
	* Create a post for our test.
	*/
	public function setUp() {
		$this->post_id = $this->factory->post->create( [
			'post_title' => 'Hello World',
			'post_content' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.',
			'post_status' => 'publish'
		] );
	}

	/**
	* Delete the post after the test.
	*/
	public function tearDown() {
		wp_delete_post( $this->post_id );
	}

	/**
	* Test to ensure quote template.
	*/
	public function test_random_quote_template() {

	}

        /**
	* Test to ensure random quote added to post.
	*/
	public function test_random_quote_post() {

	}
}

All test files and test methods should start with the keyword test for PHPUnit test to identify. Here we have used some special methods to support our test methods setUp() and tearDown(). PHPUnit also two more special methods to support in a test class which are setUpBeforeClass() and tearDownAfterClass().

setUpBeforeClass() and tearDownAfterClass() are called only once before and after the test class execution. Here we can define some of the dependencies for our test methods.

setUp() and tearDown() are called before and after each test method execution. Here we can define the dependencies for the methods.

More Info: https://phpunit.readthedocs.io/en/7.5/fixtures.html

/**
	* Test to ensure quote template.
	*/
	public function test_random_quote_template() {
		$random_quote = new Random_Quote_Wp();

		$template = $random_quote->get_quote_template();

		$this->assertInternalType( 'string', $template );

		$this->assertRegExp( '/<br\/><p><b>(.*?)<\/b><blockquote>(.*?)<\/blockquote><\/p>/', $template );
	}

	/**
	* Test to ensure random quote added to post.
	*/
	public function test_random_quote_post() {
		$content = apply_filters( 'the_content', get_post_field( 'post_content', $this->post_id ) );

		$this->assertInternalType( 'string', $content );

		$this->assertRegExp( '/<br\/><p><b>(.*?)<\/b><blockquote>(.*?)<\/blockquote><\/p>/', $content );
	}

We have written two test methods to verify two methods in Random_Quote_WP class. But it is recommended to write multiple test methods to test each method in a class based on its functionality. Since our test plugin is very simple and straightforward so I have written one test for each method in a class.

Now run the test again to see how it works.

➜ ./vendor/bin/phpunit tests/test-random-quote-wp.php

This will specifically run our test class and excludes the sample test class.

➜ random-quote-wp git:(master) ✗ ./vendor/bin/phpunit tests/test-random-quote-wp.php
Installing…
Running as single site… To run multisite, use -c tests/phpunit/multisite.xml
Not running ajax tests. To execute these, use --group ajax.
Not running ms-files tests. To execute these, use --group ms-files.
Not running external-http tests. To execute these, use --group external-http.
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

..                        2 / 2 (100%)
Time: 1.5 seconds, Memory: 28.00 MB
OK (2 tests, 4 assertions)

If you are seeing a similar result as above, then you have done it well. Now it’s your turn to expand the tests based on the class and its functionality.