Repository for my workshop at WordCamp Catania 2019
Optional, but you might need to get docker installed:
sudo apt-get install docker-ce docker-ce-cli containerd.io
I assume you have Composer installed. Let's install PHPUnit first:
composer require --dev phpunit/phpunit ^8.3
Please, check also the requirements!
PHPUnit 8.3 requires at least PHP 7.2! By the way - security support for PHP 7.1 ends on 1st of December 2019.
Hint: You don't have Composer installed? Try this!
docker run --rm -it -v $PWD:/app -u $(id -u):$(id -g) composer install
There are at least two valid frameworks that come handy when you plan to test WordPress extensions:
Let's try Brain Monkey:
composer require --dev brain/monkey:2.*`
This will automatically install also Mockery and Patchwork. Just execute composer install
and you are good to go.
Create a directory that will give a home to a small test-class named WcctaTest.php:
mkdir -p tests/wccta
Excellent! Now let's create a phpunit.xml configuration file in the root directory.
You could also decide to run your tests with the configuration parameters from the command-line. See the next part (hint: 'scripts')!
Great! Add some sections to the composer.json file:
- name: that's the project's name for packagist.org
- description: that's the description for packagist.org
- type: defines the code as WordPress plugin
- autoload: Let's use a PSR-4 autoloader!
- scripts: now you can just type
composer test
Let's create a directory that will give a home to our source-code. This is the place where you'll put a first class that you'll test soon.
mkdir -p src/wccta && touch src/wccta/Plugin.php
rm -f tests/wccta/WcctaTest.php && touch tests/wccta/PluginTest.php
touch wordpress-plugins-phpunit.php
We want to test some methods of the class Plugin
. Imagine a method called is_loaded
that returns true
on success. When you are ready, execute:
composer test
Hint: Your system or PHP version is not up to date? You could just skip this step but let's try something [not so] new!
docker run -it --rm -v $PWD:/app -w /app php:7.3-alpine php ./vendor/bin/phpunit
You can probably imagine that some plugins will have lots of classes and that you can easily forget to test all the functionalities that need testing.
So, let's talk about Coverage!
Just add a custom command to the scripts-section in your composer.json:
"coverage": "./vendor/bin/phpunit --coverage-html ./reports/php/coverage"
and a filter to your phpunit.xml:
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory>./src</directory>
</whitelist>
</filter>
Now just execute composer coverage
! This will create a directory ./reports/php/coverage
together with some html-files. Well, not on all computers. Some will still get error-messages like:
Error: No code coverage driver is available
Let's fix that in our docker-image. I prepared a Dockerfile so that you can just execute:
docker build -t coverage .
And after the build process has been finished:
docker run -it --rm -v $PWD:/app -w /app coverage:latest php ./vendor/bin/phpunit --coverage-html ./reports/php/coverage
Now you know Kung Fu! Please, open the file ./reports/php/coverage/index.html in your browser!
Let's wire our Plugin
-class to the plugin. Before we really go into testing, I'll just show you how to declare parts of your codes as not to test.
@codeCoverageIgnore
This is one of the important annotations that are available. We'll get back to this later, but first:
Run the unittests with the coverage-report again!
You did maybe notice the column CRAP
in the coverage report. CRAP is an acronym for change risk anti-patterns. It indicates how risky a change of code in a class or method can be. You can lower the risk (and therefore the index) with less complex code and full coverage with tests.
Let's start to test something. But what? There is still no further functionality written that needs testing.
Here comes TDD (Test Driven Development) into the game.
Even if you decide not to use this technique, you should at least know what we are talking about.
Let's first create a Test CarTest
that should test if the method get_price
returns the string '€ 14.500'
. Then create a Class Car
and write the method get_price
that satisfies the test. Don't start with the implementation.
At this point let me introduce also the testing pattern AAA (Arrange Act Assert) which is widely accepted in TDD. It describes how to arrange a test and is very similar to GWT (Given When Then) from BDD (Behavior-driven Development).
You can test your classes if they throw exception in certain conditions.
Let's now implement the get_price
-method.
Just create a class Registry
that sets a mixed value as a named item in an internal array. Use a method set()
or the magic method __set()
for this.
First of all assume that we can pass a JSON-object to our Car
-class. This will give our class a bit more value.
Another method get
or __get()
should check if an item with a given exists and return it on success. If there is no such item, throw an \OutOfBoundsException
.
Now write a constructor that handles the JSON input and stores the object in a member-var data
. The get_price
-method should take the price from the data
var and take care of the formatted output.
Check branch step-10 out if you have a hard time to write the code!
The variable price
should be an integer. This is probably no problem right now because you can use the PHP-function number_format()
to create the correct output. But in a WordPress installation you'll expect to have the locale set, to it_IT
(Italian) for example.
The correct way to format numbers in WordPress is the use of the function number_format_i18n()
.
So let's change that and see what happens:
Error: Call to undefined function wccta\number_format_i18n()
We will fix this in a second, but let's prepare this a bit first. Brain Monkey uses the setUp()
and tearDown()
provided by PHPUnit. You can override those methods. Let's create a custom TestCase
- name it WcctaCase
- that we can extend because we'll do this probably in every test-class.
Now let's include the namespace for tests in the section autoload-dev:
"autoload-dev": {
"psr-4": {
"tests\\wccta\\": "tests/wccta"
}
},
Finally, let's change the parent of our test-classes.
class CarTest extends WcctaTestCase { // ... }
We are ready to mock our first WordPress-function with
Functions\expect( $name_of_function )->andReturn( $value );
Writing a test for just one expectation seems too much effort. What if you want to test against different values?
Dataprovider to the rescue. I already talked about annotations in step 5. This one is also very useful:
@dataprovider method_that_returns_data
Have a look at my example. getData
returns an array of arrays. Each of these arrays contains 3 values. Our test_getPrice
-method can so not only accept the dataprovider with the annotation, but it can also define the input-vars as parameters.
You can test your classes if they throw exception in certain conditions.
Just create a class Registry
that sets a mixed value as a named item in an internal array. Use a method set()
or the magic method __set()
for this.
Another method get
or __get()
should check if an item with a given key exists and returns it on success. If there is no such item, throw an \OutOfBoundsException
.
Check branch step-10 out if you have a hard time to write the code!
The last steps brought us to Factories. What is a factory? Sometimes you create functions or methods that simply hide the complex process to create a specific object. And sometimes you have to decide which type of object you want to create.
In WordPress plugins I prefer to add hooks in factories to objects. There are plugins that add hooks in class-constructors. This is not a good thing (especially when you still test the classic way -creating a complete environment with WordPress up and running).
Let's create a class Factory
with a static function named create
. This method should return a Car
object. But let's refactor the constructor of Car
so that it expects already an object and no JSON-string. We will do this in the create-method of the Factory
-class instead.
Test your plugin now with composer test
and you'll see some errors:
TypeError: Argument 1 passed to wccta\Car::__construct() must be an object, string given, called in ...
We should correct our tests too...
Excellent! Let's create a test for our Factory. We will let the method without any content for now. Run the tests again!
There was 1 risky test:
1) tests\wccta\FactoryTest::test_create
This test did not perform any assertions
The tests pass but you get the message that there was a risky test. By the way: Name the function test_create
just create
and use the annotation @test
. I believe that the use of that annotation depends on your personal taste!
We will now dive a bit deeper into this.
Create an interface FooterInterface
that defines a public method info
which won't expect any return value. Implement the interface in Car
, info
could - for example - output a funny message.
Define the return type FooterInterface
for the create
-method of Factory
and add the info
-method of Car
to the WordPress-Action wp_footer
.
Now let's test this in the FactoryTest
. There are at least two ways to test this properly. Use has_action or Actions\expectAdded()
. A test for filters would be similar and is well described on the linked page.
Check if composer test
still passes all tests.
How is the coverage right now? Execute composer coverage
and check the generated output.
The info
-method of our Car
-class is not covered by any test. But can we test the output of a method?
Turns out it is quite easy with expectOutputString.
Let's celebrate what we learned!
Create a class Locale
that has a public method get
that returns get_locale()
. Exclude the method from coverage!
Now create a constructor in our Plugin
-class that accepts a Locale
-instance and store it in a member-var $this->locale
. Create then a method get_region_code
that returns the value of $this->locale->get()
. Ah, and remove the is_loaded
-method. ;)
In our test we could create an object of type Locale
, mock the WordPress-function get_locale
and pass it to the Plugin
-constructor! But I want tuse Mocker here:
public function test_get_region_code() {
$code = 'it_IT';
$locale = \Mockery::mock( Locale::class );
$locale->shouldReceive( 'get' )->andReturn( $code );
$sut = new Plugin( $locale );
$this->assertEquals( $code, $sut->get_region_code() );
}
Now you can go and make your WordPress-plugins bulletproof!
Have fun!