If you haven’t heard, Liquid Web is now the first company offering Managed WooCommerce hosting, which is a huge step forward in the world of WordPress-oriented e-commerce. As a result, I’ve been spending a lot of time over the last few weeks working on WooCommerce extensions that help improve the experience and performance of WooCommerce.
One of the main WooCommerce extensions I’ve been working on is WooCommerce Custom Orders Table, which takes the WooCommerce 3.x CRUD concept to its next logical point: storing order data in a custom, flat table instead of scattered throughout post meta. Mindsize worked with other members of my team at Liquid Web to build the initial version of the plugin, then I came in to fix a few bugs.
It started with a callback not being registered properly. Hoping to prevent regressions in the future, I used WP-CLI to scaffold some PHPUnit testing for the plugin, then went to work fixing the issue. Next, I discovered that there were some improperly-prepared database queries, so I would start writing tests and refactoring. It seemed every thread I pulled unravelled another set of issues, and I was exploring the codebase (both the plugin and WooCommerce itself) by writing tests.
Finally, I reached the point where I needed to be able to generate new products and orders in my tests. “This has got to be a problem that’s already been solved,” I thought as I dug into WooCommerce core to see how they were handling test data. As it turns out, WooCommerce eschews the typical WordPress factories (e.g. $this->factory()->post->create()
) in favor of a series of helper classes with static methods.
In WooCommerce core, generating an order might look something like this:
1 2 3 4 5 6 |
public function test_order_is_created() { $order = WC_Helper_Order::create_order(); $query = new WC_Order_Query(); $this->assertCount( 1, $results->get_orders() ); } |
While it doesn’t offer quite the same level of flexibility as the default WordPress factories, this is a perfectly valid way to quickly generate a new order for test purposes. Now, if only there was a way to use these helpers in my tests… ?
Putting the “WooCommerce” into “WooCommerce Extensions”
The more I looked at how WooCommerce was handling its tests, the more I realized how much effort I was duplicating by trying to tie my plugin’s test bootstrap into WooCommerce. WooCommerce Custom Orders Table fundamentally alters the way that WooCommerce handles order data, so testing the plugin in isolation makes little sense; if something works in WooCommerce without the plugin, it should still work when the plugin is active.
I decided to take a new approach to testing the plugin: extending WooCommerce’s core test suite. Thanks to Composer, I could easily load WooCommerce as a development dependency:
1 |
$ composer require --dev --prefer-source woocommerce/woocommerce |
Next, I popped open the composer.json
file to ensure that WooCommerce’s core tests were included in the Composer-generated autoloader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "name": "liquidweb/woocommerce-custom-orders-table", "description": "Store WooCommerce order data in a custom table for improved performance", "type": "wordpress-plugin", "require-dev": { "woocommerce/woocommerce": "dev-master", }, "autoload-dev": { "classmap": [ "vendor/woocommerce/woocommerce/tests/framework" ] }, "extra": { "installer-paths": { "vendor/{$vendor}/{$name}": [ "woocommerce/woocommerce" ] } } } |
Pay close attention to the “extra” node at the bottom there: WooCommerce ships with the composer/installers package by default, so Composer will attempt to install WooCommerce to ./wp-content/plugins/woocommerce
by default, instead of the traditional ./vendor/{vendor}/{name}
structure.
After running composer update
, Composer generates a fresh copy of the autoloader that includes all of the classes from WooCommerce’s tests/framework/
directory.
Extending the WooCommerce test suite
Now that we have access to WooCommerce from within our local project, it’s time to leverage it in our tests!
I’d recommend starting by either writing a new base test case or updating your test cases to extend the WC_Unit_Test_Case
class. Either way, your test files will look something like this:
1 2 3 4 5 6 7 |
/** * Tests for some feature within my WooCommerce extension. */ class MyExtensionTest extends WC_Unit_Test_Case { /* ... */ } |
Extending WC_Unit_Test_Case
ensures that your tests will get the same setup/tear-down treatment as the rest of WooCommerce, saving you a lot of unnecessary bootstrapping logic.
Speaking of bootstrapping, the last file we need to edit is the PHPUnit bootstrap file (tests/bootstrap.php
, by default). I’ve added some extra to the WooCommerce Custom Orders Table plugin, but the general idea is:
- Determine the system paths for the test installation of WordPress and the WooCommerce
tests/bootstrap.php
file. - If either of those are missing, throw an error and exit with a non-zero exit code.
- Load the WordPress core test framework’s
include/functions.php
file, which grants access to thetests_add_filter()
method. This will be used to manually load your WooCommerce extension on “muplugins_loaded”. - Require the Composer-generated autoloader, then finally the WooCommerce core test bootstrap file.
Put together, your WooCommerce extension’s bootstrap file may look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php /** * Bootstrap the PHPUnit test suite(s). */ // Determine where the test WordPress environment lives. $_tests_dir = getenv( 'WP_TESTS_DIR' ) ? getenv( 'WP_TESTS_DIR' ) : rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; // Find the path to the WooCommerce core test bootstrap file. $_bootstrap = dirname( __DIR__ ) . '/vendor/woocommerce/woocommerce/tests/bootstrap.php'; // Verify that Composer dependencies have been installed. if ( ! file_exists( $_bootstrap ) ) { echo "Unable to find the WooCommerce test bootstrap file. Have you run `composer install`?"; exit( 1 ); } elseif ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) { echo "Could not find $_tests_dir/includes/functions.php, have you run `tests/bin/install-wp-tests.sh`?"; exit( 1 ); } // Gives access to tests_add_filter() function. require_once $_tests_dir . '/includes/functions.php'; // Manually load the plugin on muplugins_loaded. function _manually_load_plugin() { require dirname( dirname( __FILE__ ) ) . '/your-plugin-file.php'; } tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); // Finally, Start up the WP testing environment. require_once dirname( __DIR__ ) . '/vendor/autoload.php'; require_once $_bootstrap; |
Running the tests
At this point, you should be able to run your test suite, with full access to WooCommerce functionality and test helpers. Running PHPUnit as you would normally, a test WordPress instance with WooCommerce should be spun up, and your tests executed. If that’s all you’re looking for, congratulations!
What if you need to test how WooCommerce behaves when your plugin is active, though? In our example, we’re fundamentally altering the way that WooCommerce handles orders; wouldn’t it be nice to know if we broke things elsewhere within WooCommerce?
This is where PHPUnit’s test suites come in handy: By defining two separate suites, we can separate our WooCommerce extension tests from WooCommerce core. In our phpunit.xml
file, we can define multiple <testsuites />
, which can then be addressed by name:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?xml version="1.0"?> <phpunit bootstrap="tests/bootstrap.php" backupGlobals="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" > <testsuites> <!-- Plugin-specific tests. --> <testsuite name="plugin"> <directory prefix="test-" suffix=".php">./tests/</directory> <exclude>tests/test-sample.php</exclude> </testsuite> <!-- WooCommerce core test suite. --> <testsuite name="core"> <directory suffix=".php">./vendor/woocommerce/woocommerce/tests/unit-tests</directory> </testsuite> </testsuites> </phpunit> |
Now, PHPUnit can be run using only the plugin tests, only the WooCommerce core tests, or with all tests:
1 2 3 4 5 6 7 8 |
# Run all tests. $ phpunit # Run only the plugin tests. $ phpunit --testsuite=plugin # Run only WooCommerce core tests. $ phpunit --testsuite=core |
Making WooCommerce work for you
One of the beautiful things about extending the core test suite in our WooCommerce extensions is that we have a figurative “canary in the coal mine” if anything breaks in future releases of WooCommerce. If a filter we’re relying on is removed, or some method is rewritten in a way we hadn’t anticipated, our tests will let us know before our users run into troubles.
There’s another side to this reliance on the core test suite, too: it motivates you to contribute back to WooCommerce. In the last few weeks, I’ve spent more time digging through WooCommerce core than I’d ever thought possible. In some cases, I found areas that are important to the WooCommerce Custom Orders Table plugin, but simply didn’t have any test coverage within WooCommerce. Instead of writing more tests in the plugin, I submitted several pull requests against WooCommerce itself.
In total, my PRs would add about 2% code coverage to WooCommerce, all focused in uncovered (or under-covered) areas that our WooCommerce extension relies on. More tests for WooCommerce mean greater coverage within our plugin, while also helping other WooCommerce extensions at the same time.
James Golovich
Glad to have found this, I was working on coming up with an easy way to do something similar and wasn’t having much luck. This makes it really easy to use GitHub, Travis, and phpunit to test against multiple WooCommerce versions.
One note though. “composer require –dev –prefer-src woocommerce/woocommerce” should be using –prefer-source not –prefer-src
Steve
Glad it’s helpful!
You’re also 100% correct on
--prefer-source
, I’ve updated the post. Thank you!BrianHenryIE
Thanks. In
_manually_load_plugin
, I also had to addrequire_once $plugin_root_dir . '/vendor/woocommerce/woocommerce/woocommerce.php';
PHP Fatal error: Class ‘WC_…’ not found in …`as I was getting
jacobmciiab
I had a similar issue. I got around it by calling the WC bootstrap file before the autoloader.
Tim Nolte
So, I had begun using this method to add unit tests to a WooCommerce plugin I co-maintain. I just found out, through performing some updates, that as of WooCommerce 3.8 this no longer works as their Testing Suite has changed significantly. For now I’ve set the WooCommerce version to be fixed at 3.7.x in Composer and this will get me by in the short term. I’m hoping to find an alternative to this.
nitinatlogys
We are trying to do the phpunit for the custom develop payment gateway , but we can’t able to do it successfully.
Can you please help us to do the phpunit for the Plugin before go to production.
See the below we getting an error,
PHP Fatal error: Class ‘WC_Payment_Gateway’ not found in /var/www/html/{PLUGIN_ROOT}/wp-content/plugins/{PLUGIN_ROOT_FOLDER}/classes/class-wc-peach-payments.php on line 14
Fatal error: Class ‘WC_Payment_Gateway’ not found in /var/www/html/{PLUGIN_ROOT}/wp-content/plugins/{PLUGIN_ROOT_FOLDER}/classes/class-wc-peach-payments.php on line 14