{"id":15434597,"url":"https://github.com/lloc/wordpress-plugins-phpunit","last_synced_at":"2026-02-27T09:11:31.886Z","repository":{"id":141903842,"uuid":"205118943","full_name":"lloc/wordpress-plugins-phpunit","owner":"lloc","description":"Repository for the workshop \"Unittests for WordPress plugins (without WP)\"","archived":false,"fork":false,"pushed_at":"2019-09-21T14:26:43.000Z","size":74,"stargazers_count":10,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-10-18T08:54:21.917Z","etag":null,"topics":["coverage","mock","php","phpunit","unittests","wordpress","wordpress-plugin","workshop"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lloc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-08-29T08:39:22.000Z","updated_at":"2022-09-22T08:51:02.000Z","dependencies_parsed_at":null,"dependency_job_id":"d4ef21ad-7b89-4c31-b5f7-3720054ca3b2","html_url":"https://github.com/lloc/wordpress-plugins-phpunit","commit_stats":{"total_commits":41,"total_committers":1,"mean_commits":41.0,"dds":0.0,"last_synced_commit":"fb863e868a1a6a8c4b18694e845da0afb463a0f6"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lloc%2Fwordpress-plugins-phpunit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lloc%2Fwordpress-plugins-phpunit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lloc%2Fwordpress-plugins-phpunit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lloc%2Fwordpress-plugins-phpunit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lloc","download_url":"https://codeload.github.com/lloc/wordpress-plugins-phpunit/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249759443,"owners_count":21321728,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coverage","mock","php","phpunit","unittests","wordpress","wordpress-plugin","workshop"],"created_at":"2024-10-01T18:40:12.729Z","updated_at":"2026-02-27T09:11:26.865Z","avatar_url":"https://github.com/lloc.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Unittests for WordPress plugins (without WP)\n\nRepository for my workshop at [WordCamp Catania 2019](https://2019.catania.wordcamp.org/)\n\n_Optional, but you might need to [get docker](https://docs.docker.com/install/) installed:_\n                       \n```\nsudo apt-get install docker-ce docker-ce-cli containerd.io\n```\n\n---\n\n## Step 1\n\nI assume you have [Composer](https://getcomposer.org/) installed. Let's install [PHPUnit](https://phpunit.de/) first:\n\n```\ncomposer require --dev phpunit/phpunit ^8.3\n```\n\nPlease, check also the [requirements](https://phpunit.readthedocs.io/en/8.3/installation.html#requirements)!\n\n\u003e PHPUnit 8.3 requires at least PHP 7.2! By the way - security support for PHP 7.1 ends on 1st of December 2019.\n\n_Hint_: You don't have **Composer** installed? Try this!\n\n```\ndocker run --rm -it -v $PWD:/app -u $(id -u):$(id -g) composer install\n```\n \n## Step 2\n\nThere are at least two valid frameworks that come handy when you plan to test WordPress extensions:\n\n- [WP_Mock](https://github.com/10up/wp_mock)\n- [Brain Monkey](https://brain-wp.github.io/BrainMonkey/)\n\nLet's try **Brain Monkey**:\n\n```\ncomposer require --dev brain/monkey:2.*`\n```\n\nThis will automatically install also [Mockery](http://docs.mockery.io/en/latest/) and [Patchwork](http://patchwork2.org/). Just execute `composer install` and you are good to go.\n\n## Step 3\n\nCreate a directory that will give a home to a small test-class named _WcctaTest.php_:\n\n```\nmkdir -p tests/wccta\n```\n\nExcellent! Now let's create a *phpunit.xml* configuration file in the root directory.\n\n\u003e You could also decide to run your tests with the configuration parameters from the command-line. See the next part (hint: 'scripts')!  \n\nGreat! Add some sections to the *composer.json* file:\n\n- **name**: that's the project's name for packagist.org\n- **description**: that's the description for packagist.org\n- **type**: defines the code as WordPress plugin \n- **autoload**: Let's use a PSR-4 autoloader!\n- **scripts**: now you can just type `composer test`\n\n## Step 4\n\nLet'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.\n\n```\nmkdir -p src/wccta \u0026\u0026 touch src/wccta/Plugin.php\nrm -f tests/wccta/WcctaTest.php \u0026\u0026 touch tests/wccta/PluginTest.php\ntouch wordpress-plugins-phpunit.php\n```\n\nWe 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:\n\n```\ncomposer test\n```\n\n_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!\n        \n```\ndocker run -it --rm -v $PWD:/app -w /app php:7.3-alpine php ./vendor/bin/phpunit\n```\n \nYou 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.\n\nSo, let's talk about __Coverage__!\n\nJust add a custom command to the scripts-section in your *composer.json*:\n\n```\n\"coverage\": \"./vendor/bin/phpunit --coverage-html ./reports/php/coverage\"\n```\n\nand a filter to your *phpunit.xml*:\n\n```\n\u003cfilter\u003e\n    \u003cwhitelist processUncoveredFilesFromWhitelist=\"true\"\u003e\n        \u003cdirectory\u003e./src\u003c/directory\u003e\n    \u003c/whitelist\u003e\n\u003c/filter\u003e\n```\n\nNow 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:\n\n```\nError:         No code coverage driver is available\n```\n\nLet's fix that in our docker-image. I prepared a _Dockerfile_ so that you can just execute:\n\n```                                    \ndocker build -t coverage .\n```\n\nAnd after the build process has been finished:\n\n```\ndocker run -it --rm -v $PWD:/app -w /app coverage:latest php ./vendor/bin/phpunit --coverage-html ./reports/php/coverage\n```\n    \n_Now you know Kung Fu!_ Please, open the file _./reports/php/coverage/index.html_ in your browser!\n\n## Step 5\n\nLet'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.\n\n```\n@codeCoverageIgnore\n```\n\nThis is one of the important [annotations](https://phpunit.readthedocs.io/en/8.3/annotations.html) that are available. We'll get back to this later, but first:\n\n_Run the unittests with the coverage-report again!_\n\nYou 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.\n    \n## Step 6\n\nLet's start to test something. But what? There is still no further functionality written that needs testing.\n \nHere comes [TDD](https://it.wikipedia.org/wiki/Test_driven_development) (Test Driven Development) into the game.\n\nEven if you decide _not_ to use this technique, you should at least know what we are talking about.\n\nLet'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.\n\nAt 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](https://en.wikipedia.org/wiki/Behavior-driven_development) (Behavior-driven Development).\n\n## Step 7\n\nYou can test your classes if they throw exception in certain conditions.\nLet's now implement the `get_price`-method.\n\nJust 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.\nFirst of all assume that we can pass a JSON-object to our `Car`-class. This will give our class a bit more value.\n\nAnother 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`.\nNow 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.\n\nCheck branch _step-10_ out if you have a hard time to write the code!\nThe 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.\n\n## Step 8\n\nThe correct way to format numbers in _WordPress_ is the use of the function `number_format_i18n()`.\n\nSo let's change that and see what happens:\n\n`Error: Call to undefined function wccta\\number_format_i18n()`\n\nWe 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](https://brain-wp.github.io/BrainMonkey/docs/wordpress-setup.html). Let's create a custom `TestCase` - name it `WcctaCase` - that we can extend because we'll do this probably in every test-class.\n\nNow let's include the namespace for tests in the section autoload-dev:\n\n```\n\"autoload-dev\": {\n    \"psr-4\": {\n        \"tests\\\\wccta\\\\\": \"tests/wccta\"\n    }\n},\n```\n\nFinally, let's change the parent of our test-classes.\n\n```\nclass CarTest extends WcctaTestCase { // ... }\n```\n\nWe are ready to mock our first _WordPress_-function with\n\n```\nFunctions\\expect( $name_of_function )-\u003eandReturn( $value );\n```\n\n## Step 9\n\nWriting a test for just one expectation seems too much effort. What if you want to test against different values?\n\nDataprovider to the rescue. I already talked about annotations in step 5. This one is also very useful:\n\n    @dataprovider method_that_returns_data\n    \nHave 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.\n\n## Step 10\n\nYou can test your classes if they throw exception in certain conditions.\n\nJust 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.\n\nAnother 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`.\n\nCheck branch _step-10_ out if you have a hard time to write the code!\n\n## Step 11\n\nThe 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.\n\nIn 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).\n\nLet'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.\n\nTest your plugin now with `composer test` and you'll see some errors:\n\n`TypeError: Argument 1 passed to wccta\\Car::__construct() must be an object, string given, called in ...`\n\nWe should correct our tests too...\n\nExcellent! Let's create a test for our Factory. We will let the method without any content for now. Run the tests again!\n\n```\nThere was 1 risky test:\n \n1) tests\\wccta\\FactoryTest::test_create\nThis test did not perform any assertions\n```\n\nThe 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! \n\n## Step 12\n\nWe will now dive a bit deeper into this.\n\nCreate 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.\n\nDefine the return type `FooterInterface` for the `create`-method of `Factory` and add the `info`-method of `Car` to the WordPress-Action `wp_footer`.\n\nNow let's test this in the `FactoryTest`. There are at least two ways to test this properly. Use [has_action](https://brain-wp.github.io/BrainMonkey/docs/wordpress-hooks-added.html) or `Actions\\expectAdded()`. A test for filters would be similar and is well described on the linked page.\n\nCheck if `composer test` still passes all tests.\n\n## Step 13\n\nHow is the coverage right now? Execute `composer coverage` and check the generated output.\n\nThe `info`-method of our `Car`-class is not covered by any test. But can we test the output of a method?\n\nTurns out it is quite easy with [expectOutputString](https://phpunit.readthedocs.io/en/8.3/writing-tests-for-phpunit.html?highlight=expectOutputString).\n\n## Finale\n\nLet's celebrate what we learned!\n\nCreate a class `Locale` that has a public method `get` that returns `get_locale()`. Exclude  the method from coverage!\n\nNow create a constructor in our `Plugin`-class that accepts a `Locale`-instance and store it in a member-var `$this-\u003elocale`. Create then a method `get_region_code` that returns the value of `$this-\u003elocale-\u003eget()`. Ah, and remove the `is_loaded`-method. ;)\n\nIn 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:\n\n```\npublic function test_get_region_code() {\n    $code   = 'it_IT';\n    $locale = \\Mockery::mock( Locale::class );\n    $locale-\u003eshouldReceive( 'get' )-\u003eandReturn( $code );\n\n    $sut = new Plugin( $locale );\n\n    $this-\u003eassertEquals( $code, $sut-\u003eget_region_code() );\n}\n```\n\n**Now you can go and make your WordPress-plugins bulletproof!**\n\n_Have fun!_\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flloc%2Fwordpress-plugins-phpunit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flloc%2Fwordpress-plugins-phpunit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flloc%2Fwordpress-plugins-phpunit/lists"}