From 1f965c7de278e3bb0d5733355a4a02f9cf767c1a Mon Sep 17 00:00:00 2001 From: Fulvio Notarstefano Date: Fri, 17 Mar 2023 19:25:40 +0800 Subject: [PATCH 1/7] Move documentation to Gitbook --- CREDITS.md | 14 - README.md | 481 +--------------------------------- docs/general/configuration.md | 65 +++++ docs/general/installation.md | 28 ++ docs/general/introduction.md | 9 + docs/introduction.md | 2 - docs/summary.md | 10 +- docs/temporary-file.md | 364 +++++++++++++++++++++++++ 8 files changed, 481 insertions(+), 492 deletions(-) delete mode 100644 CREDITS.md create mode 100644 docs/general/configuration.md create mode 100644 docs/general/installation.md create mode 100644 docs/general/introduction.md delete mode 100644 docs/introduction.md create mode 100644 docs/temporary-file.md diff --git a/CREDITS.md b/CREDITS.md deleted file mode 100644 index 81d844e7..00000000 --- a/CREDITS.md +++ /dev/null @@ -1,14 +0,0 @@ -The following acknowledges the Maintainers for this repository, those who have Contributed to this repository (via bug reports, code, design, ideas, project management, translation, testing, etc.), and any Libraries utilized. - -## Maintainers - -The following individuals are responsible for curating the list of issues, responding to pull requests, and ensuring regular releases happen. - -[Jeffrey Paul (@jeffpaul)](https://github.com/jeffpaul), [Fulvio Notarstefano (@unfulvio-godaddy)](https://github.com/unfulvio-godaddy), and the GoDaddy Managed WooCommerce Engineering team. - -## Contributors - -Thank you to all the people who have already contributed to this repository via bug reports, code, design, ideas, project management, translation, testing, etc. - -[Eric Mann (@ericmann)](https://github.com/ericmann), [John P. Bloch (@johnpbloch)](https://github.com/johnpbloch), [Nicolas VINCENT (@nicolqs)](https://github.com/nicolqs), [Alex Khadiwala (@khadiwaa)](https://github.com/khadiwaa), [Kat Hagan (@codebykat)](https://github.com/codebykat), [Jeff Sebring (@jeffsebring)](https://github.com/jeffsebring), [Giuseppe Mazzapica (@gmazzap)](https://github.com/gmazzap), [Luís Rodrigues (@goblindegook)](https://github.com/goblindegook), [Steve Grunwell (@stevegrunwell)](https://github.com/stevegrunwell), [Sudar Muthu (@sudar)](https://github.com/sudar), [Thorsten Frommen (@tfrommen)](https://github.com/tfrommen), [Darshan Sawardekar (@dsawardekar)](https://github.com/dsawardekar), [Gary Jones (@GaryJones)](https://github.com/GaryJones), [Payton Swick (@sirbrillig)](https://github.com/sirbrillig), [Pete Nelson (@petenelson)](https://github.com/petenelson), [Taylor Lovett (@tlovett1)](https://github.com/tlovett1), [Mathieu Hays (@mathieuhays)](https://github.com/mathieuhays), [Luke Woodward (@lkwdwrd)](https://github.com/lkwdwrd), [Chris Marslender (@cmmarslender)](https://github.com/cmmarslender), [Brian Watson (@bswatson)](https://github.com/bswatson), [Krody Robert (@krodyrobi)](https://github.com/krodyrobi), [Chris Wiseman](), [Andrea Sciamanna](), [Patrick Safarov (@psafarov)](https://github.com/psafarov), [Aaron (@ocularrhythm)](https://github.com/ocularrhythm), [Ramy Deeb (@rdeeb)](https://github.com/rdeeb), [Christopher Watts (@rocketcrazy07)](https://github.com/rocketcrazy07), [Jeffrey Paul (@jeffpaul)](https://github.com/jeffpaul), [Brian Henry (@BrianHenryIE)](https://github.com/BrianHenryIE), [Konstantinos Pappas (@over-engineer)](https://github.com/over-engineer), [Fedir Kudinov (@kudinovfedor](https://github.com/kudinovfedor), [Jignesh Nakrani (@jigneshnakrani088)](https://github.com/jigneshnakrani088), [Zach O (@phatsk)](https://github.com/phatsk), [Ryan Neudorf (@ohryan)](https://github.com/ohryan), [Willington Vega (@wvega)](https://github.com/wvega), [Fulvio Notarstefano (@unfulvio)](https://github.com/unfulvio), [Ashley Gibson (@ashleyfae)](https://github.com/ashleyfae). - diff --git a/README.md b/README.md index 83a7bcc5..4e241389 100644 --- a/README.md +++ b/README.md @@ -4,490 +4,21 @@ [![Support Level](https://img.shields.io/badge/support-active-green.svg)](#support-level) ![PHP 7.3+][php-image] [![Coverage Status][coveralls-image]][coveralls-url] [![Packagist][packagist-image]][packagist-url] [![GPLv2 License](https://img.shields.io/badge/license-GPL--2.0-orange)](https://github.com/10up/wp_mock/blob/trunk/LICENSE.md) -## Table of Contents -* [Installation](#installation) - * [Bootstrapping WP_Mock](#bootstrapping-wp_mock) - * [Strict Mode](#strict-mode) -* [Using WP_Mock](#using-wp_mock) - * [Mocking WordPress core functions](#mocking-wordpress-core-functions) - * [Using Mockery expectations](#using-mockery-expectations) - * [Passthru functions](#passthru-functions) - * [Deprecated methods](#deprecated-methods) - * [Mocking actions and filters](#mocking-actions-and-filters) - * [Mocking WordPress objects](#mocking-wordpress-objects) - * [Mocking constants](#mocking-constants) -* [Changelog](#changelog) -* [Contributing](#contributing) - ## Installation -First, add WP Mock as a dev-dependency with [Composer](http://getcomposer.org): - -```bash -composer require --dev 10up/wp_mock:dev-trunk -``` - -_**Note:** you may specify any tagged version other than `dev-trunk`._ - -Then, make sure your bootstrap file is loading the composer autoloader: - -```php -require_once 'vendor/autoload.php'; -``` - -Finally, register calls inside your test class to instantiate and clean up the `WP_Mock` object: - -```php -class MyTestClass extends \WP_Mock\Tools\TestCase { - public function setUp(): void { - \WP_Mock::setUp(); - } - - public function tearDown(): void { - \WP_Mock::tearDown(); - } -} -``` - -### Bootstrapping WP_Mock - -#### bootstrap.php -Before you can start using WP_Mock to test your code, you'll need to bootstrap the library by creating a `bootstrap.php` file. - -Here is an example of a bootstrap you might use: - -```php - 42, - 'times' => 1, - 'return' => 'http://example.com/foo' - ) ); - - \WP_Mock::passthruFunction( 'absint', array( 'times' => 1 ) ); - - \WP_Mock::onFilter( 'special_filter' ) - ->with( 'http://example.com/foo' ) - ->reply( 'https://example.com/bar' ); - - \WP_Mock::expectAction( 'special_action', 'https://example.com/bar' ); - - $result = my_permalink_function( 42 ); - - $this->assertEquals( 'https://example.com/bar', $result ); - } -} -``` - -The function being described by our tests would look something like this: - -```php -/** - * Get a post's permalink, then run it through special filters and trigger - * the 'special_action' action hook. - * - * @param int $post_id The post ID being linked to. - * @return str|bool The permalink or a boolean false if $post_id does - * not exist. - */ -function my_permalink_function( $post_id ) { - $permalink = get_permalink( absint( $post_id ) ); - $permalink = apply_filters( 'special_filter', $permalink ); - - do_action( 'special_action', $permalink ); - - return $permalink; -} -``` - -### Mocking WordPress core functions - -Ideally, a unit test will not depend on WordPress being loaded in order to test our code. By constructing **mocks**, it's possible to simulate WordPress core functionality by defining their expected arguments, responses, the number of times they are called, and more. In WP_Mock, this is done via the `\WP_Mock::userFunction()` method: - -```php -public function test_uses_get_post() { - global $post; - - $post = new \stdClass; - $post->ID = 42; - $post->special_meta = '

I am on the end

'; - - \WP_Mock::userFunction( 'get_post', array( - 'times' => 1, - 'args' => array( $post->ID ), - 'return' => $post, - ) ); - - /* - * Let's say our function gets the post and appends a value stored in - * 'special_meta' to the content. - */ - $results = special_the_content( '

Some content

' ); - - /* - * In addition to failing if this assertion is false, the test will fail - * if get_post is not called with the arguments above. - */ - $this->assertEquals( '

Some content

I am on the end

', $results ); -} -``` - -In the example above, we're creating a simple `\stdClass` to represent a response from `get_post()`, setting the `ID` and `special_meta` properties. WP_Mock is expecting `get_post()` to be called exactly once, with a single argument of '42', and for the function to return our `$post` object. - -With our expectations set, we call `special_the_content()`, the function we're testing, then asserting that what we get back from it is equal to `

Some content

I am on the end

`, which proves that `special_the_content()` appended `$post->special_meta` to `

Some content

`. - -Calling `\WP_Mock::userFunction()` will dynamically define the function for you if necessary, which means changes the internal WP_Mock API shouldn't break your mocks. If you really want to define your own function mocks, they should always end with this line: - -```php -return \WP_Mock\Handler::handle_function( __FUNCTION__, func_get_args() ); -``` - -#### Setting expectations - -`\WP_Mock::userFunction()` accepts an associative array of arguments for its second parameter: - -##### args - -Sets expectations about what the arguments passed to the function should be. This value should always be an array with the arguments in order and, like with return, if you use a `\Closure`, its return value will be used to validate the argument expectations. You can also indicate that the argument can be any value of any type by using '`*`'. - -WP_Mock has several helper functions to make this feature more flexible. The are static methods on the `\WP_Mock\Functions` class. They are: - -* `Functions::type( $type )`: Expects an argument of a certain type. This can be any core PHP data type (`string`, `int`, `resource`, `callable`, etc.) or any class or interface name. -* `Functions::anyOf( $values )`: Expects the argument to be any value in the `$values` array. - -###### Examples - -In the following example, we're expecting `get_post_meta()` twice: once each for `some_meta_key` and `another_meta_key`, where an integer (in this case, a post ID) is the first argument, the meta key is the second, and a boolean TRUE is the third. - -```php -\WP_Mock::userFunction( 'get_post_meta', array( - 'times' => 1, - 'args' => array( \WP_Mock\Functions::type( 'int' ), 'some_meta_key', true ) -) ); - -\WP_Mock::userFunction( 'get_post_meta', array( - 'times' => 1, - 'args' => array( \WP_Mock\Functions::type( 'int' ), 'another_meta_key', true ) -) ); -``` - -##### times - -Declares how many times the given function should be called. For an exact number of calls, use a non-negative, numeric value (e.g. `3`). If the function should be called a minimum number of times, append a plus-sign (`+`, e.g. `7+` for seven or more calls). Conversely, if a mocked function should have a maximum number of invocations, append a minus-sign (`-`) to the argument (e.g. `7-` for seven or fewer times). - -You may also choose to specify a range, e.g. `3-6` would translate to "this function should be called between three and six times". - -The default value for `times` is `0+`, meaning the function should be called any number of times. - -##### return - -Defines the value (if any) that the function should return. If you pass a `\Closure` as the return value, the function will return whatever the Closure's return value is. - -##### return_in_order - -Set an array of values that should be returned with each subsequent call, useful if if your function will be called multiple times in the test but needs to return different values. - -**Note:** Setting this value overrides whatever may be set `return`. - -###### Example - -```php -\WP_Mock::userFunction( 'is_single', array( - 'return_in_order' => array( true, false ) -) ); - -$this->assertTrue( is_single() ); -$this->assertFalse( is_single() ); -$this->assertFalse( is_single() ); // All subsequent calls will use the last defined return value -``` -##### return_arg - -Use this to specify that the function should return one of its arguments. `return_arg` should be the position of the argument in the arguments array, so `0` for the first argument, `1` for the second, etc. You can also set this to `true`, which is equivalent to `0`. This will override both `return` and `return_in_order`. - -### Using Mockery expectations - -The return value of `\WP_Mock::userFunction` will be a complete `Mockery\Mock` object with any expectations added to match the arguments passed to the function. This enables using [Mockery methods](http://docs.mockery.io/en/latest/reference/expectations.html) to add expectations in addition to, or instead of using the arguments array passed to `userFunction`. - -For example, the following are synonymous: - -```php -\WP_Mock::userFunction( 'get_permalink', array( 'args' => 42, 'return' => 'http://example.com/foo' ) ); -``` +Install WP_Mock as a dev-dependency using Composer: ```php -\WP_Mock::userFunction( 'get_permalink' )->with( 42 )->andReturn( 'http://example.com/foo' ); +composer require --dev 10up/wp_mock ``` -### Passthru functions - -It's not uncommon for tests to need to declare "passthrough/passthru" functions: empty functions that just return whatever they're passed (remember: you're testing your code, not the framework). In these situations you can use `\WP_Mock::passthruFunction( 'function_name' )`, which is equivalent to the following: - -```php -\WP_Mock::userFunction( 'function_name', array( - 'return_arg' => 0 -) ); -``` - -You can still test things like invocation count by passing the `times` argument in the second parameter, just like `\WP_Mock::userFunction()`. - -### Deprecated methods - -Please note that `WP_Mock::wpFunction()` and `WP_Mock::wpPassthruFunction()` are both officially deprecated. Replace all uses of them with `WP_Mock::userFunction()` and `WP_Mock::passthruFunction()`. If you use either of the deprecated methods, WP_Mock will mark those tests as risky. Your tests will still count as passing, but PHPUnit will start telling you which tests are causing issues. - -### Mocking actions and filters - -The [hooks and filters of the WordPress Plugin API](http://codex.wordpress.org/Plugin_API) are common (and preferred) entry points for third-party scripts, and WP_Mock makes it easy to test that these are being registered and executed within your code. - -#### Ensuring actions and filters are registered - -Rather than attempting to mock `add_action()` or `add_filter()`, WP_Mock has built-in support for both of these functions: instead, use `\WP_Mock::expectActionAdded()` and `\WP_Mock::expectFilterAdded()` (respectively). In the following example, our `test_special_function()` test will fail if `special_function()` doesn't call `add_action( 'save_post', 'special_save_post', 10, 2 )` _and_ `add_filter( 'the_content', 'special_the_content' )`: - -```php -public function test_special_function() { - \WP_Mock::expectActionAdded( 'save_post', 'special_save_post', 10, 2 ); - \WP_Mock::expectFilterAdded( 'the_content', 'special_the_content' ); - - special_function(); -} -``` - -It's important to note that the `$priority` and `$parameter_count` arguments (parameters 3 and 4 for both `add_action()` and `add_filter()`) are significant. If `special_function()` were to call `add_action( 'save_post', 'special_save_post', 99, 3 )` instead of the expected `add_action( 'save_post', 'special_save_post', 10, 2 )`, our test would fail. - -If the actual instance of an expected class cannot be passed, `AnyInstance` can be used: - -```php -\WP_Mock::expectFilterAdded( 'the_content', array( new \WP_Mock\Matcher\AnyInstance( Special::class ), 'the_content' ) ); -``` - -#### Asserting that closures have been added as hook callbacks - -Sometimes it's handy to add a [Closure](https://secure.php.net/manual/en/class.closure.php) as a WordPress hook instead of defining a function in the global namespace. To assert that such a hook has been added, you can perform assertions referencing the Closure class or a `callable` type: - -```php -public function test_anonymous_function_hook() { - \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type('callable')); - \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type(Closure::class)); - \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type('callable')); - \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type(Closure::class)); -} -``` - -#### Asserting that actions and filters are applied - -Now that we're testing whether or not we're adding actions and/or filters, the next step is to ensure our code is calling those hooks when expected. - -For actions, we'll want to listen for `do_action()` to be called for our action name, so we'll use `\WP_Mock::expectAction()`: - -```php -function test_action_calling_function () { - \WP_Mock::expectAction( 'my_action' ); - - action_calling_function(); -} -``` - -This test will fail if `action_calling_function()` doesn't call `do_action( 'my_action' )`. In situations where your code needs to trigger actions, this assertion makes sure the appropriate hooks are being triggered. - -For filters, we can inject our own response to `apply_filters()` using `\WP_Mock::onFilter()`: - -```php -public function filter_content() { - return apply_filters( 'custom_content_filter', 'This is unfiltered' ); -} - -public function test_filter_content() { - \WP_Mock::onFilter( 'custom_content_filter' ) - ->with( 'This is unfiltered' ) - ->reply( 'This is filtered' ); - - $response = $this->filter_content(); - - $this->assertEquals( 'This is filtered', $response ); -} -``` - -Alternatively, there is a method `\WP_Mock::expectFilter()` that will add a bare assertion that the filter will be applied without changing the value: - -```php -class SUT { - public function filter_content() { - $value = apply_filters( 'custom_content_filter', 'Default' ); - if ( $value === 'Default' ) { - do_action( 'default_value' ); - } - - return $value; - } -} - -class SUTTest { - public function test_filter_content() { - \WP_Mock::expectFilter( 'custom_content_filter', 'Default' ); - \WP_Mock::expectAction( 'default_value' ); - - $this->assertEquals( 'Default', (new SUT)->filter_content() ); - } -} -``` - -### Mocking WordPress objects - -Mocking calls to `wpdb`, `WP_Query`, etc. can be done using the [mockery](https://github.com/padraic/mockery) framework. While this isn't part of WP Mock itself, complex code will often need these objects and this framework will let you incorporate those into your tests. Since WP Mock requires Mockery, it should already be included as part of your install. - -#### $wpdb example - -Let's say we have a function that gets three post IDs from the database. -```php -function get_post_ids() { - global $wpdb; - return $wpdb->get_col( "select ID from {$wpdb->posts} LIMIT 3" ); -} -``` - -When we mock the `$wpdb` object, we're not performing an actual database call, only mocking the results. We need to call the `get_col` method with an SQL statement, and return three arbitrary post IDs. - -```php -use Mockery; - -function test_get_post_ids() { - global $wpdb; - - $wpdb = Mockery::mock( '\WPDB' ); - $wpdb->shouldReceive( 'get_col' ) - ->once() - ->with( "select ID from wp_posts LIMIT 3" ) - ->andReturn( array( 1, 2, 3 ) ); - $wpdb->posts = 'wp_posts'; - - $post_ids = get_post_ids(); - - $this->assertEquals( array( 1, 2, 3 ), $post_ids ); -} -``` - -### Mocking constants - -Certain constants need to be mocked, otherwise various WordPress functions will attempt to include files that just don't exist. - -For example, nearly all uses of the `WP_Http` API require first including: - -``` -ABSPATH . WPINC . '/class-http.php' -``` - -If these constants are not set, and files do not exist at the location they specify, functions referencing them will fatally err. - -By default, WP_Mock will [mock the following constants](./php/WP_Mock/API/constant-mocks.php): - -| Constant | Default mocked value | -|------------------|----------------------------------------| -| `WP_CONTENT_DIR` | `__DIR__ . '/dummy-files'` | -| `ABSPATH` | `''` | -| `WPINC` | `__DIR__ . '/dummy-files/wp-includes'` | -| `EZSQL_VERSION` | `'WP1.25'` | -| `OBJECT` | `'OBJECT'` | -| `Object` | `'OBJECT'` | -| `object` | `'OBJECT'` | -| `OBJECT_K` | `'OBJECT_K'` | -| `ARRAY_A` | `'ARRAY_A'` | -| `ARRAY_N` | `'ARRAY_N'` | - -WP_Mock provides a few dummy files, located in the `./php/WP_Mock/API/dummy-files/` directory. These files are used to mock the `WP_CONTENT_DIR` and `WPINC` constants, as shown in the table above. - -The `! defined` check is used for all constants, so that individual test environments can override the normal default by setting constants in a bootstrap configuration file. - -## Support Level - -**Active:** 10up is actively working on this, and we expect to continue work for the foreseeable future including keeping tested up to the most recent version of WordPress. Bug reports, feature requests, questions, and pull requests are welcome. - -## Changelog +## Documentation -A complete listing of all notable changes to WP_Mock are documented in [CHANGELOG.md](https://github.com/10up/wp_mock/blob/trunk/CHANGELOG.md). +Learn more about configuring and using WP_Mock by reading [the WP_Mock documentation](https://wp-mock.gitbook.io/documentation/general/introduction). ## Contributing -Please read [CODE_OF_CONDUCT.md](https://github.com/10up/wp_mock/blob/trunk/CODE_OF_CONDUCT.md) for details on our code of conduct, [CONTRIBUTING.md](https://github.com/10up/wp_mock/blob/trunk/CONTRIBUTING.md) for details on the process for submitting pull requests to us, and [CREDITS.md](https://github.com/10up/wp_mock/blob/trunk/CREDITS.md) for a listing of maintainers of, contributors to, and libraries used by WP_Mock. +Please read [CODE_OF_CONDUCT.md](https://github.com/10up/wp_mock/blob/trunk/CODE_OF_CONDUCT.md) for details on our code of conduct and [CONTRIBUTING.md](https://github.com/10up/wp_mock/blob/trunk/CONTRIBUTING.md) for details on the process for submitting pull requests. ## Like what you see? @@ -497,4 +28,4 @@ Please read [CODE_OF_CONDUCT.md](https://github.com/10up/wp_mock/blob/trunk/CODE [packagist-image]: https://img.shields.io/packagist/dt/10up/wp_mock.svg [packagist-url]: https://packagist.org/packages/10up/wp_mock [coveralls-image]: https://coveralls.io/repos/github/10up/wp_mock/badge.svg?branch=trunk -[coveralls-url]: https://coveralls.io/github/10up/wp_mock?branch=trunk +[coveralls-url]: https://coveralls.io/github/10up/wp_mock?branch=trunk \ No newline at end of file diff --git a/docs/general/configuration.md b/docs/general/configuration.md new file mode 100644 index 00000000..8a96ff70 --- /dev/null +++ b/docs/general/configuration.md @@ -0,0 +1,65 @@ +# Configuration + +After installing WP_Mock you will need to perform some simple configuration steps before you can start using it in your tests. + +## Bootstrap WP_Mock + +Before you can start using WP_Mock to test your code, you'll need to bootstrap the library by creating a bootstrap.php file. + +Here is an example of a bootstrap you might use: + +```php + 42, + 'times' => 1, + 'return' => 'http://example.com/foo' + ) ); + + \WP_Mock::passthruFunction( 'absint', array( 'times' => 1 ) ); + + \WP_Mock::onFilter( 'special_filter' ) + ->with( 'http://example.com/foo' ) + ->reply( 'https://example.com/bar' ); + + \WP_Mock::expectAction( 'special_action', 'https://example.com/bar' ); + + $result = my_permalink_function( 42 ); + + $this->assertEquals( 'https://example.com/bar', $result ); + } +} +``` + +The function being described by our tests would look something like this: + +```php +/** + * Get a post's permalink, then run it through special filters and trigger + * the 'special_action' action hook. + * + * @param int $post_id The post ID being linked to. + * @return str|bool The permalink or a boolean false if $post_id does + * not exist. + */ +function my_permalink_function( $post_id ) { + $permalink = get_permalink( absint( $post_id ) ); + $permalink = apply_filters( 'special_filter', $permalink ); + + do_action( 'special_action', $permalink ); + + return $permalink; +} +``` + +### Mocking WordPress core functions + +Ideally, a unit test will not depend on WordPress being loaded in order to test our code. By constructing **mocks**, it's possible to simulate WordPress core functionality by defining their expected arguments, responses, the number of times they are called, and more. In WP_Mock, this is done via the `\WP_Mock::userFunction()` method: + +```php +public function test_uses_get_post() { + global $post; + + $post = new \stdClass; + $post->ID = 42; + $post->special_meta = '

I am on the end

'; + + \WP_Mock::userFunction( 'get_post', array( + 'times' => 1, + 'args' => array( $post->ID ), + 'return' => $post, + ) ); + + /* + * Let's say our function gets the post and appends a value stored in + * 'special_meta' to the content. + */ + $results = special_the_content( '

Some content

' ); + + /* + * In addition to failing if this assertion is false, the test will fail + * if get_post is not called with the arguments above. + */ + $this->assertEquals( '

Some content

I am on the end

', $results ); +} +``` + +In the example above, we're creating a simple `\stdClass` to represent a response from `get_post()`, setting the `ID` and `special_meta` properties. WP_Mock is expecting `get_post()` to be called exactly once, with a single argument of '42', and for the function to return our `$post` object. + +With our expectations set, we call `special_the_content()`, the function we're testing, then asserting that what we get back from it is equal to `

Some content

I am on the end

`, which proves that `special_the_content()` appended `$post->special_meta` to `

Some content

`. + +Calling `\WP_Mock::userFunction()` will dynamically define the function for you if necessary, which means changes the internal WP_Mock API shouldn't break your mocks. If you really want to define your own function mocks, they should always end with this line: + +```php +return \WP_Mock\Handler::handle_function( __FUNCTION__, func_get_args() ); +``` + +#### Setting expectations + +`\WP_Mock::userFunction()` accepts an associative array of arguments for its second parameter: + +##### args + +Sets expectations about what the arguments passed to the function should be. This value should always be an array with the arguments in order and, like with return, if you use a `\Closure`, its return value will be used to validate the argument expectations. You can also indicate that the argument can be any value of any type by using '`*`'. + +WP_Mock has several helper functions to make this feature more flexible. The are static methods on the `\WP_Mock\Functions` class. They are: + +* `Functions::type( $type )`: Expects an argument of a certain type. This can be any core PHP data type (`string`, `int`, `resource`, `callable`, etc.) or any class or interface name. +* `Functions::anyOf( $values )`: Expects the argument to be any value in the `$values` array. + +###### Examples + +In the following example, we're expecting `get_post_meta()` twice: once each for `some_meta_key` and `another_meta_key`, where an integer (in this case, a post ID) is the first argument, the meta key is the second, and a boolean TRUE is the third. + +```php +\WP_Mock::userFunction( 'get_post_meta', array( + 'times' => 1, + 'args' => array( \WP_Mock\Functions::type( 'int' ), 'some_meta_key', true ) +) ); + +\WP_Mock::userFunction( 'get_post_meta', array( + 'times' => 1, + 'args' => array( \WP_Mock\Functions::type( 'int' ), 'another_meta_key', true ) +) ); +``` + +##### times + +Declares how many times the given function should be called. For an exact number of calls, use a non-negative, numeric value (e.g. `3`). If the function should be called a minimum number of times, append a plus-sign (`+`, e.g. `7+` for seven or more calls). Conversely, if a mocked function should have a maximum number of invocations, append a minus-sign (`-`) to the argument (e.g. `7-` for seven or fewer times). + +You may also choose to specify a range, e.g. `3-6` would translate to "this function should be called between three and six times". + +The default value for `times` is `0+`, meaning the function should be called any number of times. + +##### return + +Defines the value (if any) that the function should return. If you pass a `\Closure` as the return value, the function will return whatever the Closure's return value is. + +##### return_in_order + +Set an array of values that should be returned with each subsequent call, useful if if your function will be called multiple times in the test but needs to return different values. + +**Note:** Setting this value overrides whatever may be set `return`. + +###### Example + +```php +\WP_Mock::userFunction( 'is_single', array( + 'return_in_order' => array( true, false ) +) ); + +$this->assertTrue( is_single() ); +$this->assertFalse( is_single() ); +$this->assertFalse( is_single() ); // All subsequent calls will use the last defined return value +``` +##### return_arg + +Use this to specify that the function should return one of its arguments. `return_arg` should be the position of the argument in the arguments array, so `0` for the first argument, `1` for the second, etc. You can also set this to `true`, which is equivalent to `0`. This will override both `return` and `return_in_order`. + +### Using Mockery expectations + +The return value of `\WP_Mock::userFunction` will be a complete `Mockery\Mock` object with any expectations added to match the arguments passed to the function. This enables using [Mockery methods](http://docs.mockery.io/en/latest/reference/expectations.html) to add expectations in addition to, or instead of using the arguments array passed to `userFunction`. + +For example, the following are synonymous: + +```php +\WP_Mock::userFunction( 'get_permalink', array( 'args' => 42, 'return' => 'http://example.com/foo' ) ); +``` + +```php +\WP_Mock::userFunction( 'get_permalink' )->with( 42 )->andReturn( 'http://example.com/foo' ); +``` + +### Passthru functions + +It's not uncommon for tests to need to declare "passthrough/passthru" functions: empty functions that just return whatever they're passed (remember: you're testing your code, not the framework). In these situations you can use `\WP_Mock::passthruFunction( 'function_name' )`, which is equivalent to the following: + +```php +\WP_Mock::userFunction( 'function_name', array( + 'return_arg' => 0 +) ); +``` + +You can still test things like invocation count by passing the `times` argument in the second parameter, just like `\WP_Mock::userFunction()`. + + + + +### Mocking actions and filters + +The [hooks and filters of the WordPress Plugin API](http://codex.wordpress.org/Plugin_API) are common (and preferred) entry points for third-party scripts, and WP_Mock makes it easy to test that these are being registered and executed within your code. + +#### Ensuring actions and filters are registered + +Rather than attempting to mock `add_action()` or `add_filter()`, WP_Mock has built-in support for both of these functions: instead, use `\WP_Mock::expectActionAdded()` and `\WP_Mock::expectFilterAdded()` (respectively). In the following example, our `test_special_function()` test will fail if `special_function()` doesn't call `add_action( 'save_post', 'special_save_post', 10, 2 )` _and_ `add_filter( 'the_content', 'special_the_content' )`: + +```php +public function test_special_function() { + \WP_Mock::expectActionAdded( 'save_post', 'special_save_post', 10, 2 ); + \WP_Mock::expectFilterAdded( 'the_content', 'special_the_content' ); + + special_function(); +} +``` + +It's important to note that the `$priority` and `$parameter_count` arguments (parameters 3 and 4 for both `add_action()` and `add_filter()`) are significant. If `special_function()` were to call `add_action( 'save_post', 'special_save_post', 99, 3 )` instead of the expected `add_action( 'save_post', 'special_save_post', 10, 2 )`, our test would fail. + +If the actual instance of an expected class cannot be passed, `AnyInstance` can be used: + +```php +\WP_Mock::expectFilterAdded( 'the_content', array( new \WP_Mock\Matcher\AnyInstance( Special::class ), 'the_content' ) ); +``` + +#### Asserting that closures have been added as hook callbacks + +Sometimes it's handy to add a [Closure](https://secure.php.net/manual/en/class.closure.php) as a WordPress hook instead of defining a function in the global namespace. To assert that such a hook has been added, you can perform assertions referencing the Closure class or a `callable` type: + +```php +public function test_anonymous_function_hook() { + \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type('callable')); + \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type(Closure::class)); + \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type('callable')); + \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type(Closure::class)); +} +``` + +#### Asserting that actions and filters are applied + +Now that we're testing whether or not we're adding actions and/or filters, the next step is to ensure our code is calling those hooks when expected. + +For actions, we'll want to listen for `do_action()` to be called for our action name, so we'll use `\WP_Mock::expectAction()`: + +```php +function test_action_calling_function () { + \WP_Mock::expectAction( 'my_action' ); + + action_calling_function(); +} +``` + +This test will fail if `action_calling_function()` doesn't call `do_action( 'my_action' )`. In situations where your code needs to trigger actions, this assertion makes sure the appropriate hooks are being triggered. + +For filters, we can inject our own response to `apply_filters()` using `\WP_Mock::onFilter()`: + +```php +public function filter_content() { + return apply_filters( 'custom_content_filter', 'This is unfiltered' ); +} + +public function test_filter_content() { + \WP_Mock::onFilter( 'custom_content_filter' ) + ->with( 'This is unfiltered' ) + ->reply( 'This is filtered' ); + + $response = $this->filter_content(); + + $this->assertEquals( 'This is filtered', $response ); +} +``` + +Alternatively, there is a method `\WP_Mock::expectFilter()` that will add a bare assertion that the filter will be applied without changing the value: + +```php +class SUT { + public function filter_content() { + $value = apply_filters( 'custom_content_filter', 'Default' ); + if ( $value === 'Default' ) { + do_action( 'default_value' ); + } + + return $value; + } +} + +class SUTTest { + public function test_filter_content() { + \WP_Mock::expectFilter( 'custom_content_filter', 'Default' ); + \WP_Mock::expectAction( 'default_value' ); + + $this->assertEquals( 'Default', (new SUT)->filter_content() ); + } +} +``` + + + + +### Mocking WordPress objects + +Mocking calls to `wpdb`, `WP_Query`, etc. can be done using the [mockery](https://github.com/padraic/mockery) framework. While this isn't part of WP Mock itself, complex code will often need these objects and this framework will let you incorporate those into your tests. Since WP Mock requires Mockery, it should already be included as part of your install. + +#### $wpdb example + +Let's say we have a function that gets three post IDs from the database. +```php +function get_post_ids() { + global $wpdb; + return $wpdb->get_col( "select ID from {$wpdb->posts} LIMIT 3" ); +} +``` + +When we mock the `$wpdb` object, we're not performing an actual database call, only mocking the results. We need to call the `get_col` method with an SQL statement, and return three arbitrary post IDs. + +```php +use Mockery; + +function test_get_post_ids() { + global $wpdb; + + $wpdb = Mockery::mock( '\WPDB' ); + $wpdb->shouldReceive( 'get_col' ) + ->once() + ->with( "select ID from wp_posts LIMIT 3" ) + ->andReturn( array( 1, 2, 3 ) ); + $wpdb->posts = 'wp_posts'; + + $post_ids = get_post_ids(); + + $this->assertEquals( array( 1, 2, 3 ), $post_ids ); +} +``` + +### Mocking constants + +Certain constants need to be mocked, otherwise various WordPress functions will attempt to include files that just don't exist. + +For example, nearly all uses of the `WP_Http` API require first including: + +``` +ABSPATH . WPINC . '/class-http.php' +``` + +If these constants are not set, and files do not exist at the location they specify, functions referencing them will fatally err. + +By default, WP_Mock will [mock the following constants](./php/WP_Mock/API/constant-mocks.php): + +| Constant | Default mocked value | +|------------------|----------------------------------------| +| `WP_CONTENT_DIR` | `__DIR__ . '/dummy-files'` | +| `ABSPATH` | `''` | +| `WPINC` | `__DIR__ . '/dummy-files/wp-includes'` | +| `EZSQL_VERSION` | `'WP1.25'` | +| `OBJECT` | `'OBJECT'` | +| `Object` | `'OBJECT'` | +| `object` | `'OBJECT'` | +| `OBJECT_K` | `'OBJECT_K'` | +| `ARRAY_A` | `'ARRAY_A'` | +| `ARRAY_N` | `'ARRAY_N'` | + +WP_Mock provides a few dummy files, located in the `./php/WP_Mock/API/dummy-files/` directory. These files are used to mock the `WP_CONTENT_DIR` and `WPINC` constants, as shown in the table above. + +The `! defined` check is used for all constants, so that individual test environments can override the normal default by setting constants in a bootstrap configuration file. + From dd243e0720ef748dcd4edd586a84765b17619d50 Mon Sep 17 00:00:00 2001 From: Fulvio Notarstefano Date: Tue, 21 Mar 2023 15:51:19 +0800 Subject: [PATCH 2/7] Update docs --- .editorconfig | 2 + docs/summary.md | 5 +- docs/temporary-file.md | 364 ------------------ docs/usage/mocking-action-and-filter-hooks.md | 95 +++++ docs/usage/mocking-wordpress-objects.md | 48 +++ docs/usage/mocking-wp-constants.md | 30 ++ docs/usage/using-wp-mock.md | 157 ++++++++ 7 files changed, 336 insertions(+), 365 deletions(-) delete mode 100644 docs/temporary-file.md create mode 100644 docs/usage/mocking-action-and-filter-hooks.md create mode 100644 docs/usage/mocking-wordpress-objects.md create mode 100644 docs/usage/mocking-wp-constants.md create mode 100644 docs/usage/using-wp-mock.md diff --git a/.editorconfig b/.editorconfig index 40fc2fa0..5db71332 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,6 +4,8 @@ root = true [*.md] +indent_style = space +indent_size = 4 charset = utf-8 end_of_line = lf diff --git a/docs/summary.md b/docs/summary.md index 026c5c1d..67f159e9 100644 --- a/docs/summary.md +++ b/docs/summary.md @@ -8,4 +8,7 @@ ## Usage -* [Using WP_Mock](usage/using-wp-mock.md) \ No newline at end of file +* [Using WP_Mock](usage/using-wp-mock.md) +* [Mocking WP Hooks](usage/mocking-action-and-filter-hooks.md) +* [Mocking WP Objects](usage/mocking-wordpress-objects.md) +* [Mocking WP Constants](usage/mocking-wordpress-constants.md) \ No newline at end of file diff --git a/docs/temporary-file.md b/docs/temporary-file.md deleted file mode 100644 index a03eceef..00000000 --- a/docs/temporary-file.md +++ /dev/null @@ -1,364 +0,0 @@ - - -## Using WP_Mock - -Write your tests as you normally would. If you desire specific responses from WordPress API calls, wire those specifically. - -```php -namespace MyPlugin\Tests\Unit; - -use WP_Mock\Tools\TestCase; - -/** - * @covers MyPlugin\MyClass - */ -final class MyClassTest extends TestCase -{ - /** - * Assume that my_permalink_function() is meant to do all of the following: - * - Run the given post ID through absint() - * - Call get_permalink() on the $post_id - * - Pass the permalink through the 'special_filter' filter - * - Trigger the 'special_action' WordPress action - */ - public function test_my_permalink_function() { - \WP_Mock::userFunction( 'get_permalink', array( - 'args' => 42, - 'times' => 1, - 'return' => 'http://example.com/foo' - ) ); - - \WP_Mock::passthruFunction( 'absint', array( 'times' => 1 ) ); - - \WP_Mock::onFilter( 'special_filter' ) - ->with( 'http://example.com/foo' ) - ->reply( 'https://example.com/bar' ); - - \WP_Mock::expectAction( 'special_action', 'https://example.com/bar' ); - - $result = my_permalink_function( 42 ); - - $this->assertEquals( 'https://example.com/bar', $result ); - } -} -``` - -The function being described by our tests would look something like this: - -```php -/** - * Get a post's permalink, then run it through special filters and trigger - * the 'special_action' action hook. - * - * @param int $post_id The post ID being linked to. - * @return str|bool The permalink or a boolean false if $post_id does - * not exist. - */ -function my_permalink_function( $post_id ) { - $permalink = get_permalink( absint( $post_id ) ); - $permalink = apply_filters( 'special_filter', $permalink ); - - do_action( 'special_action', $permalink ); - - return $permalink; -} -``` - -### Mocking WordPress core functions - -Ideally, a unit test will not depend on WordPress being loaded in order to test our code. By constructing **mocks**, it's possible to simulate WordPress core functionality by defining their expected arguments, responses, the number of times they are called, and more. In WP_Mock, this is done via the `\WP_Mock::userFunction()` method: - -```php -public function test_uses_get_post() { - global $post; - - $post = new \stdClass; - $post->ID = 42; - $post->special_meta = '

I am on the end

'; - - \WP_Mock::userFunction( 'get_post', array( - 'times' => 1, - 'args' => array( $post->ID ), - 'return' => $post, - ) ); - - /* - * Let's say our function gets the post and appends a value stored in - * 'special_meta' to the content. - */ - $results = special_the_content( '

Some content

' ); - - /* - * In addition to failing if this assertion is false, the test will fail - * if get_post is not called with the arguments above. - */ - $this->assertEquals( '

Some content

I am on the end

', $results ); -} -``` - -In the example above, we're creating a simple `\stdClass` to represent a response from `get_post()`, setting the `ID` and `special_meta` properties. WP_Mock is expecting `get_post()` to be called exactly once, with a single argument of '42', and for the function to return our `$post` object. - -With our expectations set, we call `special_the_content()`, the function we're testing, then asserting that what we get back from it is equal to `

Some content

I am on the end

`, which proves that `special_the_content()` appended `$post->special_meta` to `

Some content

`. - -Calling `\WP_Mock::userFunction()` will dynamically define the function for you if necessary, which means changes the internal WP_Mock API shouldn't break your mocks. If you really want to define your own function mocks, they should always end with this line: - -```php -return \WP_Mock\Handler::handle_function( __FUNCTION__, func_get_args() ); -``` - -#### Setting expectations - -`\WP_Mock::userFunction()` accepts an associative array of arguments for its second parameter: - -##### args - -Sets expectations about what the arguments passed to the function should be. This value should always be an array with the arguments in order and, like with return, if you use a `\Closure`, its return value will be used to validate the argument expectations. You can also indicate that the argument can be any value of any type by using '`*`'. - -WP_Mock has several helper functions to make this feature more flexible. The are static methods on the `\WP_Mock\Functions` class. They are: - -* `Functions::type( $type )`: Expects an argument of a certain type. This can be any core PHP data type (`string`, `int`, `resource`, `callable`, etc.) or any class or interface name. -* `Functions::anyOf( $values )`: Expects the argument to be any value in the `$values` array. - -###### Examples - -In the following example, we're expecting `get_post_meta()` twice: once each for `some_meta_key` and `another_meta_key`, where an integer (in this case, a post ID) is the first argument, the meta key is the second, and a boolean TRUE is the third. - -```php -\WP_Mock::userFunction( 'get_post_meta', array( - 'times' => 1, - 'args' => array( \WP_Mock\Functions::type( 'int' ), 'some_meta_key', true ) -) ); - -\WP_Mock::userFunction( 'get_post_meta', array( - 'times' => 1, - 'args' => array( \WP_Mock\Functions::type( 'int' ), 'another_meta_key', true ) -) ); -``` - -##### times - -Declares how many times the given function should be called. For an exact number of calls, use a non-negative, numeric value (e.g. `3`). If the function should be called a minimum number of times, append a plus-sign (`+`, e.g. `7+` for seven or more calls). Conversely, if a mocked function should have a maximum number of invocations, append a minus-sign (`-`) to the argument (e.g. `7-` for seven or fewer times). - -You may also choose to specify a range, e.g. `3-6` would translate to "this function should be called between three and six times". - -The default value for `times` is `0+`, meaning the function should be called any number of times. - -##### return - -Defines the value (if any) that the function should return. If you pass a `\Closure` as the return value, the function will return whatever the Closure's return value is. - -##### return_in_order - -Set an array of values that should be returned with each subsequent call, useful if if your function will be called multiple times in the test but needs to return different values. - -**Note:** Setting this value overrides whatever may be set `return`. - -###### Example - -```php -\WP_Mock::userFunction( 'is_single', array( - 'return_in_order' => array( true, false ) -) ); - -$this->assertTrue( is_single() ); -$this->assertFalse( is_single() ); -$this->assertFalse( is_single() ); // All subsequent calls will use the last defined return value -``` -##### return_arg - -Use this to specify that the function should return one of its arguments. `return_arg` should be the position of the argument in the arguments array, so `0` for the first argument, `1` for the second, etc. You can also set this to `true`, which is equivalent to `0`. This will override both `return` and `return_in_order`. - -### Using Mockery expectations - -The return value of `\WP_Mock::userFunction` will be a complete `Mockery\Mock` object with any expectations added to match the arguments passed to the function. This enables using [Mockery methods](http://docs.mockery.io/en/latest/reference/expectations.html) to add expectations in addition to, or instead of using the arguments array passed to `userFunction`. - -For example, the following are synonymous: - -```php -\WP_Mock::userFunction( 'get_permalink', array( 'args' => 42, 'return' => 'http://example.com/foo' ) ); -``` - -```php -\WP_Mock::userFunction( 'get_permalink' )->with( 42 )->andReturn( 'http://example.com/foo' ); -``` - -### Passthru functions - -It's not uncommon for tests to need to declare "passthrough/passthru" functions: empty functions that just return whatever they're passed (remember: you're testing your code, not the framework). In these situations you can use `\WP_Mock::passthruFunction( 'function_name' )`, which is equivalent to the following: - -```php -\WP_Mock::userFunction( 'function_name', array( - 'return_arg' => 0 -) ); -``` - -You can still test things like invocation count by passing the `times` argument in the second parameter, just like `\WP_Mock::userFunction()`. - - - - -### Mocking actions and filters - -The [hooks and filters of the WordPress Plugin API](http://codex.wordpress.org/Plugin_API) are common (and preferred) entry points for third-party scripts, and WP_Mock makes it easy to test that these are being registered and executed within your code. - -#### Ensuring actions and filters are registered - -Rather than attempting to mock `add_action()` or `add_filter()`, WP_Mock has built-in support for both of these functions: instead, use `\WP_Mock::expectActionAdded()` and `\WP_Mock::expectFilterAdded()` (respectively). In the following example, our `test_special_function()` test will fail if `special_function()` doesn't call `add_action( 'save_post', 'special_save_post', 10, 2 )` _and_ `add_filter( 'the_content', 'special_the_content' )`: - -```php -public function test_special_function() { - \WP_Mock::expectActionAdded( 'save_post', 'special_save_post', 10, 2 ); - \WP_Mock::expectFilterAdded( 'the_content', 'special_the_content' ); - - special_function(); -} -``` - -It's important to note that the `$priority` and `$parameter_count` arguments (parameters 3 and 4 for both `add_action()` and `add_filter()`) are significant. If `special_function()` were to call `add_action( 'save_post', 'special_save_post', 99, 3 )` instead of the expected `add_action( 'save_post', 'special_save_post', 10, 2 )`, our test would fail. - -If the actual instance of an expected class cannot be passed, `AnyInstance` can be used: - -```php -\WP_Mock::expectFilterAdded( 'the_content', array( new \WP_Mock\Matcher\AnyInstance( Special::class ), 'the_content' ) ); -``` - -#### Asserting that closures have been added as hook callbacks - -Sometimes it's handy to add a [Closure](https://secure.php.net/manual/en/class.closure.php) as a WordPress hook instead of defining a function in the global namespace. To assert that such a hook has been added, you can perform assertions referencing the Closure class or a `callable` type: - -```php -public function test_anonymous_function_hook() { - \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type('callable')); - \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type(Closure::class)); - \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type('callable')); - \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type(Closure::class)); -} -``` - -#### Asserting that actions and filters are applied - -Now that we're testing whether or not we're adding actions and/or filters, the next step is to ensure our code is calling those hooks when expected. - -For actions, we'll want to listen for `do_action()` to be called for our action name, so we'll use `\WP_Mock::expectAction()`: - -```php -function test_action_calling_function () { - \WP_Mock::expectAction( 'my_action' ); - - action_calling_function(); -} -``` - -This test will fail if `action_calling_function()` doesn't call `do_action( 'my_action' )`. In situations where your code needs to trigger actions, this assertion makes sure the appropriate hooks are being triggered. - -For filters, we can inject our own response to `apply_filters()` using `\WP_Mock::onFilter()`: - -```php -public function filter_content() { - return apply_filters( 'custom_content_filter', 'This is unfiltered' ); -} - -public function test_filter_content() { - \WP_Mock::onFilter( 'custom_content_filter' ) - ->with( 'This is unfiltered' ) - ->reply( 'This is filtered' ); - - $response = $this->filter_content(); - - $this->assertEquals( 'This is filtered', $response ); -} -``` - -Alternatively, there is a method `\WP_Mock::expectFilter()` that will add a bare assertion that the filter will be applied without changing the value: - -```php -class SUT { - public function filter_content() { - $value = apply_filters( 'custom_content_filter', 'Default' ); - if ( $value === 'Default' ) { - do_action( 'default_value' ); - } - - return $value; - } -} - -class SUTTest { - public function test_filter_content() { - \WP_Mock::expectFilter( 'custom_content_filter', 'Default' ); - \WP_Mock::expectAction( 'default_value' ); - - $this->assertEquals( 'Default', (new SUT)->filter_content() ); - } -} -``` - - - - -### Mocking WordPress objects - -Mocking calls to `wpdb`, `WP_Query`, etc. can be done using the [mockery](https://github.com/padraic/mockery) framework. While this isn't part of WP Mock itself, complex code will often need these objects and this framework will let you incorporate those into your tests. Since WP Mock requires Mockery, it should already be included as part of your install. - -#### $wpdb example - -Let's say we have a function that gets three post IDs from the database. -```php -function get_post_ids() { - global $wpdb; - return $wpdb->get_col( "select ID from {$wpdb->posts} LIMIT 3" ); -} -``` - -When we mock the `$wpdb` object, we're not performing an actual database call, only mocking the results. We need to call the `get_col` method with an SQL statement, and return three arbitrary post IDs. - -```php -use Mockery; - -function test_get_post_ids() { - global $wpdb; - - $wpdb = Mockery::mock( '\WPDB' ); - $wpdb->shouldReceive( 'get_col' ) - ->once() - ->with( "select ID from wp_posts LIMIT 3" ) - ->andReturn( array( 1, 2, 3 ) ); - $wpdb->posts = 'wp_posts'; - - $post_ids = get_post_ids(); - - $this->assertEquals( array( 1, 2, 3 ), $post_ids ); -} -``` - -### Mocking constants - -Certain constants need to be mocked, otherwise various WordPress functions will attempt to include files that just don't exist. - -For example, nearly all uses of the `WP_Http` API require first including: - -``` -ABSPATH . WPINC . '/class-http.php' -``` - -If these constants are not set, and files do not exist at the location they specify, functions referencing them will fatally err. - -By default, WP_Mock will [mock the following constants](./php/WP_Mock/API/constant-mocks.php): - -| Constant | Default mocked value | -|------------------|----------------------------------------| -| `WP_CONTENT_DIR` | `__DIR__ . '/dummy-files'` | -| `ABSPATH` | `''` | -| `WPINC` | `__DIR__ . '/dummy-files/wp-includes'` | -| `EZSQL_VERSION` | `'WP1.25'` | -| `OBJECT` | `'OBJECT'` | -| `Object` | `'OBJECT'` | -| `object` | `'OBJECT'` | -| `OBJECT_K` | `'OBJECT_K'` | -| `ARRAY_A` | `'ARRAY_A'` | -| `ARRAY_N` | `'ARRAY_N'` | - -WP_Mock provides a few dummy files, located in the `./php/WP_Mock/API/dummy-files/` directory. These files are used to mock the `WP_CONTENT_DIR` and `WPINC` constants, as shown in the table above. - -The `! defined` check is used for all constants, so that individual test environments can override the normal default by setting constants in a bootstrap configuration file. - diff --git a/docs/usage/mocking-action-and-filter-hooks.md b/docs/usage/mocking-action-and-filter-hooks.md new file mode 100644 index 00000000..6e068483 --- /dev/null +++ b/docs/usage/mocking-action-and-filter-hooks.md @@ -0,0 +1,95 @@ +# Mocking WordPress actions and filters + +The [hooks and filters of the WordPress Plugin API](http://codex.wordpress.org/Plugin_API) are common (and preferred) entry points for third-party scripts, and WP_Mock makes it easy to test that these are being registered and executed within your code. + +#### Ensuring actions and filters are registered + +Rather than attempting to mock `add_action()` or `add_filter()`, WP_Mock has built-in support for both of these functions: instead, use `\WP_Mock::expectActionAdded()` and `\WP_Mock::expectFilterAdded()` (respectively). In the following example, our `test_special_function()` test will fail if `special_function()` doesn't call `add_action( 'save_post', 'special_save_post', 10, 2 )` _and_ `add_filter( 'the_content', 'special_the_content' )`: + +```php +public function test_special_function() { + WP_Mock::expectActionAdded( 'save_post', 'special_save_post', 10, 2 ); + WP_Mock::expectFilterAdded( 'the_content', 'special_the_content' ); + + my_special_function(); +} +``` + +It's important to note that the `$priority` and `$parameter_count` arguments (parameters 3 and 4 for both `add_action()` and `add_filter()`) are significant. If `special_function()` were to call `add_action( 'save_post', 'special_save_post', 99, 3 )` instead of the expected `add_action( 'save_post', 'special_save_post', 10, 2 )`, our test would fail. + +If the actual instance of an expected class cannot be passed, `AnyInstance` can be used: + +```php +\WP_Mock::expectFilterAdded( 'the_content', array( new \WP_Mock\Matcher\AnyInstance( Special::class ), 'the_content' ) ); +``` + +#### Asserting that closures have been added as hook callbacks + +Sometimes it's handy to add a [Closure](https://secure.php.net/manual/en/class.closure.php) as a WordPress hook instead of defining a function in the global namespace. To assert that such a hook has been added, you can perform assertions referencing the Closure class or a `callable` type: + +```php +public function test_anonymous_function_hook() { + \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type('callable')); + \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type(Closure::class)); + \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type('callable')); + \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type(Closure::class)); +} +``` + +#### Asserting that actions and filters are applied + +Now that we're testing whether or not we're adding actions and/or filters, the next step is to ensure our code is calling those hooks when expected. + +For actions, we'll want to listen for `do_action()` to be called for our action name, so we'll use `\WP_Mock::expectAction()`: + +```php +function test_action_calling_function () { + \WP_Mock::expectAction( 'my_action' ); + + action_calling_function(); +} +``` + +This test will fail if `action_calling_function()` doesn't call `do_action( 'my_action' )`. In situations where your code needs to trigger actions, this assertion makes sure the appropriate hooks are being triggered. + +For filters, we can inject our own response to `apply_filters()` using `\WP_Mock::onFilter()`: + +```php +public function filter_content() { + return apply_filters( 'custom_content_filter', 'This is unfiltered' ); +} + +public function test_filter_content() { + \WP_Mock::onFilter( 'custom_content_filter' ) + ->with( 'This is unfiltered' ) + ->reply( 'This is filtered' ); + + $response = $this->filter_content(); + + $this->assertEquals( 'This is filtered', $response ); +} +``` + +Alternatively, there is a method `\WP_Mock::expectFilter()` that will add a bare assertion that the filter will be applied without changing the value: + +```php +class SUT { + public function filter_content() { + $value = apply_filters( 'custom_content_filter', 'Default' ); + if ( $value === 'Default' ) { + do_action( 'default_value' ); + } + + return $value; + } +} + +class SUTTest { + public function test_filter_content() { + \WP_Mock::expectFilter( 'custom_content_filter', 'Default' ); + \WP_Mock::expectAction( 'default_value' ); + + $this->assertEquals( 'Default', (new SUT)->filter_content() ); + } +} +``` diff --git a/docs/usage/mocking-wordpress-objects.md b/docs/usage/mocking-wordpress-objects.md new file mode 100644 index 00000000..070efc4b --- /dev/null +++ b/docs/usage/mocking-wordpress-objects.md @@ -0,0 +1,48 @@ +# Mocking WordPress objects + +Mocking calls to `wpdb`, `WP_Query`, etc. can be done using the [Mockery](https://github.com/padraic/mockery) framework. While this isn't part of WP Mock itself, complex code will often need these objects and this framework will let you incorporate those into your tests. Since WP Mock requires Mockery, it should already be included as part of your installation. + +## An example with `WPDB` + +Let's say we have a function that gets three post IDs from the database. + +```php +namespace MyPlugin; + +class MyClass +{ + public function getSomePostIds() : array + { + global $wpdb; + return $wpdb->get_col("SELECT ID FROM {$wpdb->posts} LIMIT 3"); + } +} +``` + +When we mock the `$wpdb` object, we're not performing an actual database call, only mocking the results. We need to call the `get_col` method with an SQL statement, and return three arbitrary post IDs. + +```php +use Mockery; +use MyPlugin\MyClass; +use PHPUnit\Framework\TestCase; + +final class MyClassTest extends TestCase +{ + public function testCanGetSomePostIds() : void + { + global $wpdb; + + $wpdb = Mockery::mock('WPDB'); + $wpdb->posts = 'wp_posts'; + + $wpdb->allows('get_col') + ->once() + ->with('SELECT ID FROM wp_posts LIMIT 3') + ->andReturn([1, 2, 3]); + + $postIds = (new MyClass())->getSomePostIds(); + + $this->assertEquals([1, 2, 3], $postIds); + } +} +``` diff --git a/docs/usage/mocking-wp-constants.md b/docs/usage/mocking-wp-constants.md new file mode 100644 index 00000000..0a71cf40 --- /dev/null +++ b/docs/usage/mocking-wp-constants.md @@ -0,0 +1,30 @@ +# Mocking WordPress Constants + +Certain constants need to be mocked, otherwise various WordPress functions will attempt to include files that just don't exist. + +For example, nearly all uses of the `WP_Http` API require first including: + +``` +ABSPATH . WPINC . '/class-http.php' +``` + +If these constants are not set, and files do not exist at the location they specify, functions referencing them will produce a fatal error. + +By default, WP_Mock will [mock the following constants](./php/WP_Mock/API/constant-mocks.php): + +| Constant | Default mocked value | +|------------------|----------------------------------------| +| `WP_CONTENT_DIR` | `__DIR__ . '/dummy-files'` | +| `ABSPATH` | `''` | +| `WPINC` | `__DIR__ . '/dummy-files/wp-includes'` | +| `EZSQL_VERSION` | `'WP1.25'` | +| `OBJECT` | `'OBJECT'` | +| `Object` | `'OBJECT'` | +| `object` | `'OBJECT'` | +| `OBJECT_K` | `'OBJECT_K'` | +| `ARRAY_A` | `'ARRAY_A'` | +| `ARRAY_N` | `'ARRAY_N'` | + +WP_Mock provides a few dummy files, located in the `./php/WP_Mock/API/dummy-files/` directory. These files are used to mock the `WP_CONTENT_DIR` and `WPINC` constants, as shown in the table above. + +The `! defined` check is used for all constants, so that individual test environments can override the normal default by setting constants in a bootstrap configuration file. \ No newline at end of file diff --git a/docs/usage/using-wp-mock.md b/docs/usage/using-wp-mock.md new file mode 100644 index 00000000..3bf64088 --- /dev/null +++ b/docs/usage/using-wp-mock.md @@ -0,0 +1,157 @@ +# Using WP_Mock + +With WP_Mock you can write your PHPUnit test cases as you normally would, but with the added benefit of being able to mock WordPress functions and classes. + +## Mocking WordPress core functions + +Ideally, a unit test will not depend on WordPress being loaded in order to test our code. By constructing **mocks**, it's possible to simulate WordPress core functionality by defining their expected arguments, responses, the number of times they are called, and more. In WP_Mock, this is done via the `WP_Mock::userFunction()` method: + +Suppose you have the following method in your code that uses `get_post()` to output some content. + +```php +namespace MyPlugin; + +class MyClass +{ + public function myFunction(int $postId) : string + { + $post = get_post($postId); + + return $post ? $post->post_content : 'Post not found'; + } +} +``` + +You can use `WP_Mock::userFunction()` to mock the `get_post()` function and return a mock post object: + +```php +use MyPlugin\MyClass; +use PHPUnit\Framework\TestCase; +use stdClass +use WP_Mock; + +final class MyClassTest extends TestCase +{ + public function testMyFunction() : void + { + $post = new stdClass(); + $post->post_content = 'Hello World'; + + WP_Mock::userFunction('get_post') + ->once() + ->with(123) + ->andReturn($post); + + $this->assertSame('Hello World', (MyClass::myFunction(123)); + } +} +``` + +In the above example WP_Mock is expecting that the method `MyClass::myFunction`, when invoked, it will in turn call `get_post()` exactly once, with a single argument of `123` as passed to the method's only argument, and that will return the content of a hypothetical post having that ID. + +Calling `WP_Mock::userFunction()` will dynamically define the function for you if necessary, which means changes the internal WP_Mock API shouldn't break your mocks. If you really want to define your own function mocks, they should always end with this line: + +```php +return WP_Mock\Handler::handle_function(__FUNCTION__, func_get_args()); +``` + +## Using Mockery expectations + +The `WP_Mock::userFunction()` class will return a complete `Mockery\Expectation` object with any expectations added to match the arguments passed to the function. This enables using [Mockery methods](http://docs.mockery.io/en/latest/reference/expectations.html) to add expectations in addition to, or instead of using the arguments array passed to `userFunction`. + +For example, the invocation below will set the expectation that the `get_permalink` function will be called exactly once, with the argument `42`, and that it will return the string `'https://example.com/foo'`. + +```php +WP_Mock::userFunction('get_permalink')->once()->with(42)->andReturn('https://example.com/foo'); +``` + +## Using expectations in arguments + +You can also pass an associative array of arguments to the second parameter of `WP_Mock::userFunction()` to set expectations about the function's arguments, the number of times it should be called, and what it should return. + +### Arguments + +The `args` parameter sets expectations about what the arguments passed to the function should be. This value should always be an array with the arguments in order and, like with return, if you use a `Closure`, its return value will be used to validate the argument expectations. You can also indicate that the argument can be any value of any type by using '`*`'. + +WP_Mock has several helper functions to make this feature more flexible. There are static methods on the `WP_Mock\Functions` class meant for this: + +* `Functions::type($type)`: Expects an argument of a certain type. This can be any core PHP data type (`string`, `int`, `resource`, `callable`, etc.) or any class or interface name. +* `Functions::anyOf($values)`: Expects the argument to be any value in the `$values` array. + +#### Examples + +In the following example, we're expecting `get_post_meta()` twice: once each for `some_meta_key` and `another_meta_key`, where an integer (in this case, a post ID) is the first argument, the meta key is the second, and a boolean `true` is the third. + +```php +use WP_Mock; + +WP_Mock::userFunction('get_post_meta', [ + 'times' => 1, + 'args' => [WP_Mock\Functions::type('int'), 'some_meta_key', true], +) ); + +WP_Mock::userFunction('get_post_meta', [ + 'times' => 1, + 'args' => [WP_Mock\Functions::type('int'), 'another_meta_key', true], +) ); +``` + +### Times + +The `times` argument, as shown in the previous examples, declares how many times the given function should be called. For an exact number of calls, use a non-negative, numeric value (e.g. `3`). If the function should be called a minimum number of times, append a plus-sign (`+`, e.g. `7+` for seven or more calls). Conversely, if a mocked function should have a maximum number of invocations, append a minus-sign (`-`) to the argument (e.g. `7-` for seven or fewer times). + +You may also choose to specify a range, e.g. `3-6` would translate to "this function should be called between three and six times". + +The default value for `times` is `0+`, meaning the function should be called any number of times. + +### Return + +The `return` argument defines the value (if any) that the function should return. If you pass a `Closure` as the return value, the function will return whatever the Closure's return value is. + +#### Example + +```php +WP_Mock::userFunction('get_post_meta', [ + 'return' => function($post_id, $key, $single) { + if ($key === 'some_meta_key') { + return 'some value'; + } + + return 'another value'; + } +) ); +``` + +### Return in order + +The `return_in_order` argument sets an array of values that should be returned with each subsequent call, useful if if your function will be called multiple times in the test but needs to return different values. + +**Note:** Setting this value overrides whatever may be set `return`. + +#### Example + +```php +WP_Mock::userFunction('is_single', [ + 'return_in_order' => [true, false], +]); + +$this->assertTrue(is_single()); +$this->assertFalse(is_single()); +$this->assertFalse(is_single()); // All subsequent calls will use the last defined return value +``` +### Return argument + +You can use the `return_arg` argument to specify that the function should return one of its arguments. `return_arg` should be the position of the argument in the arguments array, so `0` for the first argument, `1` for the second, etc. You can also set this to `true`, which is equivalent to `0`. This will override both `return` and `return_in_order`. + +#### Example + +```php +WP_Mock::userFunction('sanitize_title', [ + 'return_arg' => 0, +]); + +// ... + +sanitize_title($title); // WP_Mock will have this function return the value of $title as-is +``` + From aade9cd125dc238b6c1db9880b535adca676e4c1 Mon Sep 17 00:00:00 2001 From: Fulvio Notarstefano Date: Tue, 21 Mar 2023 18:35:39 +0800 Subject: [PATCH 3/7] Refactor hooks docs --- docs/summary.md | 10 +- docs/usage/mocking-action-and-filter-hooks.md | 95 ------------ .../mocking-wp-action-and-filter-hooks.md | 143 ++++++++++++++++++ ...press-objects.md => mocking-wp-objects.md} | 0 4 files changed, 148 insertions(+), 100 deletions(-) delete mode 100644 docs/usage/mocking-action-and-filter-hooks.md create mode 100644 docs/usage/mocking-wp-action-and-filter-hooks.md rename docs/usage/{mocking-wordpress-objects.md => mocking-wp-objects.md} (100%) diff --git a/docs/summary.md b/docs/summary.md index 67f159e9..c6e59103 100644 --- a/docs/summary.md +++ b/docs/summary.md @@ -1,6 +1,6 @@ # Table of contents -## General +## Getting Started * [Introduction](general/introduction.md) * [Installation](general/installation.md) @@ -8,7 +8,7 @@ ## Usage -* [Using WP_Mock](usage/using-wp-mock.md) -* [Mocking WP Hooks](usage/mocking-action-and-filter-hooks.md) -* [Mocking WP Objects](usage/mocking-wordpress-objects.md) -* [Mocking WP Constants](usage/mocking-wordpress-constants.md) \ No newline at end of file +* [Mocking WordPress Functions](usage/using-wp-mock.md) +* [Mocking WordPress Hooks](usage/mocking-wp-action-and-filter-hooks.md) +* [Mocking WordPress Objects](usage/mocking-wp-objects.md) +* [Mocking WordPress Constants](usage/mocking-wp-constants.md) \ No newline at end of file diff --git a/docs/usage/mocking-action-and-filter-hooks.md b/docs/usage/mocking-action-and-filter-hooks.md deleted file mode 100644 index 6e068483..00000000 --- a/docs/usage/mocking-action-and-filter-hooks.md +++ /dev/null @@ -1,95 +0,0 @@ -# Mocking WordPress actions and filters - -The [hooks and filters of the WordPress Plugin API](http://codex.wordpress.org/Plugin_API) are common (and preferred) entry points for third-party scripts, and WP_Mock makes it easy to test that these are being registered and executed within your code. - -#### Ensuring actions and filters are registered - -Rather than attempting to mock `add_action()` or `add_filter()`, WP_Mock has built-in support for both of these functions: instead, use `\WP_Mock::expectActionAdded()` and `\WP_Mock::expectFilterAdded()` (respectively). In the following example, our `test_special_function()` test will fail if `special_function()` doesn't call `add_action( 'save_post', 'special_save_post', 10, 2 )` _and_ `add_filter( 'the_content', 'special_the_content' )`: - -```php -public function test_special_function() { - WP_Mock::expectActionAdded( 'save_post', 'special_save_post', 10, 2 ); - WP_Mock::expectFilterAdded( 'the_content', 'special_the_content' ); - - my_special_function(); -} -``` - -It's important to note that the `$priority` and `$parameter_count` arguments (parameters 3 and 4 for both `add_action()` and `add_filter()`) are significant. If `special_function()` were to call `add_action( 'save_post', 'special_save_post', 99, 3 )` instead of the expected `add_action( 'save_post', 'special_save_post', 10, 2 )`, our test would fail. - -If the actual instance of an expected class cannot be passed, `AnyInstance` can be used: - -```php -\WP_Mock::expectFilterAdded( 'the_content', array( new \WP_Mock\Matcher\AnyInstance( Special::class ), 'the_content' ) ); -``` - -#### Asserting that closures have been added as hook callbacks - -Sometimes it's handy to add a [Closure](https://secure.php.net/manual/en/class.closure.php) as a WordPress hook instead of defining a function in the global namespace. To assert that such a hook has been added, you can perform assertions referencing the Closure class or a `callable` type: - -```php -public function test_anonymous_function_hook() { - \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type('callable')); - \WP_Mock::expectActionAdded('save_post', \WP_Mock\Functions::type(Closure::class)); - \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type('callable')); - \WP_Mock::expectFilterAdded('the_content', \WP_Mock\Functions::type(Closure::class)); -} -``` - -#### Asserting that actions and filters are applied - -Now that we're testing whether or not we're adding actions and/or filters, the next step is to ensure our code is calling those hooks when expected. - -For actions, we'll want to listen for `do_action()` to be called for our action name, so we'll use `\WP_Mock::expectAction()`: - -```php -function test_action_calling_function () { - \WP_Mock::expectAction( 'my_action' ); - - action_calling_function(); -} -``` - -This test will fail if `action_calling_function()` doesn't call `do_action( 'my_action' )`. In situations where your code needs to trigger actions, this assertion makes sure the appropriate hooks are being triggered. - -For filters, we can inject our own response to `apply_filters()` using `\WP_Mock::onFilter()`: - -```php -public function filter_content() { - return apply_filters( 'custom_content_filter', 'This is unfiltered' ); -} - -public function test_filter_content() { - \WP_Mock::onFilter( 'custom_content_filter' ) - ->with( 'This is unfiltered' ) - ->reply( 'This is filtered' ); - - $response = $this->filter_content(); - - $this->assertEquals( 'This is filtered', $response ); -} -``` - -Alternatively, there is a method `\WP_Mock::expectFilter()` that will add a bare assertion that the filter will be applied without changing the value: - -```php -class SUT { - public function filter_content() { - $value = apply_filters( 'custom_content_filter', 'Default' ); - if ( $value === 'Default' ) { - do_action( 'default_value' ); - } - - return $value; - } -} - -class SUTTest { - public function test_filter_content() { - \WP_Mock::expectFilter( 'custom_content_filter', 'Default' ); - \WP_Mock::expectAction( 'default_value' ); - - $this->assertEquals( 'Default', (new SUT)->filter_content() ); - } -} -``` diff --git a/docs/usage/mocking-wp-action-and-filter-hooks.md b/docs/usage/mocking-wp-action-and-filter-hooks.md new file mode 100644 index 00000000..fde8a9ba --- /dev/null +++ b/docs/usage/mocking-wp-action-and-filter-hooks.md @@ -0,0 +1,143 @@ +# Mocking WordPress actions and filters + +The [hooks and filters of the WordPress Plugin API](http://codex.wordpress.org/Plugin_API) are common (and preferred) entry points for third-party scripts, and WP_Mock makes it easy to test that these are being registered and executed within your code. + +## Ensuring actions and filters are registered + +Rather than attempting to mock `add_action()` or `add_filter()`, WP_Mock has built-in support for both of these functions: instead, use `WP_Mock::expectActionAdded()` and `WP_Mock::expectFilterAdded()`, respectively. + +In the following example, our expectations will fail if the `MyClass::addHooks()` method do not call `add_action('save_post', [$this, 'myActionCallback'], 10, 2 )` _and_ `add_action('the_content', [$this, 'myFilterCallback'])`: + +```php +use MyPlugin\MyClass; +use WP_Mock\Tools\TestCase as TestCase; + +final class MyClassTest extends TestCase +{ + public function testHookExpectations() : void + { + $classInstance = new MyClass(); + + WP_Mock::expectActionAdded('save_post', [$classInstance, 'myActionCallback'], 10, 2); + WP_Mock::expectFilterAdded('the_content', [$classInstance, 'myFilterCallback']); + + $classInstance->addHooks(); + } +} +``` + +It's important to note that the `$priority` and `$parameter_count` arguments (parameters 3 and 4 for both `add_action()` and `add_filter()`) are significant. If in our example our code used a different priority or a different number of arguments when setting the callbacks, the test would have failed. + +If the actual instance of an expected class cannot be passed, `AnyInstance` can be used: + +```php +WP_Mock::expectFilterAdded('the_content', [new \WP_Mock\Matcher\AnyInstance(Special::class), 'the_content']); +``` + +## Asserting that closures have been added as hook callbacks + +Sometimes it's handy to add a [Closure](https://secure.php.net/manual/en/class.closure.php) as a WordPress hook instead of defining a function in the global namespace. To assert that such a hook has been added, you can perform assertions referencing the Closure class or a `callable` type: + +```php +public function testAnonymousHookCallback() : void +{ + WP_Mock::expectActionAdded('save_post', WP_Mock\Functions::type('callable')); + WP_Mock::expectActionAdded('save_post', WP_Mock\Functions::type(Closure::class)); + WP_Mock::expectFilterAdded('the_content', WP_Mock\Functions::type('callable')); + WP_Mock::expectFilterAdded('the_content', WP_Mock\Functions::type(Closure::class)); +} +``` + +## Asserting that actions and filters are applied + +Now that we're testing if we are adding actions and/or filters, the next step is to ensure our code is calling those hooks when expected. + +For actions, we'll want to listen for `do_action()` to be called for our action name, so we'll use `WP_Mock::expectAction()`: + +```php +public function testActionCallingFunction() : void +{ + WP_Mock::expectAction('my_action'); + + MyClass::myMethod(); +} +``` + +This test will fail if `MyClass::myMethod()` does not call `do_action('my_action')`. In situations where your code needs to trigger actions, this assertion makes sure the appropriate hooks are being triggered. + +For filters, we can inject our own response to `apply_filters()` using `WP_Mock::onFilter()`. + +Take the code below, for example: + +```php +namespace MyPlugin; + +class MyClass +{ + public function filterContent() : string + { + return apply_filters('custom_content_filter', 'This is unfiltered'); + } +} +``` + +We can test that the filter is being applied by using `WP_Mock::onFilter()`: + +```php +use MyPlugin\MyClass; +use WP_Mock; +use WP_Mock\Tools\TestCase as TestCase; + +final class MyClassTest extends TestCase +{ + public function testCanFilterContent() : void + { + WP_Mock::onFilter('custom_content_filter') + ->with('This is unfiltered') + ->reply('This is filtered'); + + $content = (new MyClass())->filterContent(); + + $this->assertEquals('This is filtered', $content); + } +} +``` + +Alternatively, there is a method `WP_Mock::expectFilter()` that will add a bare assertion that the filter will be applied without changing the value: + +```php +namespace MyPlugin; + +class MyClass +{ + public function filterContent() : string + { + $value = apply_filters( 'custom_content_filter', 'Default' ); + + if ($value === 'Default') { + do_action('default_value'); + } + + return $value; + } +} +``` + +And then the test: + +```php +use MyPlugin\MyClass; +use WP_Mock; +use WP_Mock\Tools\TestCase as TestCase; + +final class MyClassTest extends TestCase +{ + public function testCanFilterContent() : void + { + WP_Mock::expectFilter('custom_content_filter', 'Default'); + WP_Mock::expectAction('default_value'); + + $this->assertEquals('Default', (new MyClass())->filterContent()); + } +} +``` \ No newline at end of file diff --git a/docs/usage/mocking-wordpress-objects.md b/docs/usage/mocking-wp-objects.md similarity index 100% rename from docs/usage/mocking-wordpress-objects.md rename to docs/usage/mocking-wp-objects.md From f1fdabc58ccabe9b1177b48ae2e5d597fb38a6d6 Mon Sep 17 00:00:00 2001 From: Fulvio Notarstefano Date: Tue, 21 Mar 2023 18:52:36 +0800 Subject: [PATCH 4/7] Document TestCase --- docs/summary.md | 6 ++- docs/tools/wp-mock-test-case.md | 92 +++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 docs/tools/wp-mock-test-case.md diff --git a/docs/summary.md b/docs/summary.md index c6e59103..b195ae5d 100644 --- a/docs/summary.md +++ b/docs/summary.md @@ -11,4 +11,8 @@ * [Mocking WordPress Functions](usage/using-wp-mock.md) * [Mocking WordPress Hooks](usage/mocking-wp-action-and-filter-hooks.md) * [Mocking WordPress Objects](usage/mocking-wp-objects.md) -* [Mocking WordPress Constants](usage/mocking-wp-constants.md) \ No newline at end of file +* [Mocking WordPress Constants](usage/mocking-wp-constants.md) + +## Tools + +* [WP_Mock Test Case](tools/wp-mock-test-case.md) \ No newline at end of file diff --git a/docs/tools/wp-mock-test-case.md b/docs/tools/wp-mock-test-case.md new file mode 100644 index 00000000..fbba5463 --- /dev/null +++ b/docs/tools/wp-mock-test-case.md @@ -0,0 +1,92 @@ +# WP_Mock Test Case + +WP_Mock comes with a base test case class that provides a number of useful methods for testing WordPress plugins and themes. + +This class is located in the `WP_Mock\Tools\TestCase` namespace, and can be used by extending it in your test classes: + +```php +use WP_Mock\Tools\TestCase as TestCase; + +final class MyTestCase extends TestCase +{ + // ... +} +``` + +WP_Mock `TestCase` extends PHPUnit own `TestCase` so all methods and assertions from the latter are available in the former. + +On top of those, WP_Mock `TestCase` will include a collection of handy methods for helping you test your code. + +## Methods + + +### Assert conditions met + +The `TestCase::assertConditionsMet()` function will assert that the current test conditions have been met. This is useful when your test assertions are purely WP_Mock expectations, and you don't want to have to call `Mockery::close()` in your test, otherwise PHPUnit might raise a warning that no assertions were performed. + +```php +use WP_Mock\Tools\TestCase as TestCase; + +final class MyTestCase extends TestCase +{ + public function testMyFunction() : void + { + WP_Mock::userFunction('my_function', ['times' => 1]); + + $this->assertConditionsMet(); + } +} +``` + +### Assert equals HTML + +The `TestCase::assertEqualsHtml()` function will evaluate a string as HTML and compare it to another string. This is useful when you want to compare HTML strings that may have different formatting, but are otherwise identical. + +```php +use WP_Mock\Tools\TestCase as TestCase; + +final class MyTestCase extends TestCase +{ + public function testMyFunction() : void + { + $this->assertEqualsHtml('
Test
', '
Test
'); + } +} +``` + +### Expect output string + +The `TestCase::expectOutputString()` function will assert that the output of a function matches a given string. This is useful when you want to test the output of a function that echoes HTML. + +```php +use WP_Mock\Tools\TestCase as TestCase; + +final class MyTestCase extends TestCase +{ + public function testMyFunction() : void + { + $this->expectOutputString('
Test
'); + + echo '
Test
'; + } +} +``` + +### Mock static method + +The `TestCase::mockStaticMethod()` function will mock a static method on a class, via Patchwork, returning a Mockery object. + +```php +use WP_Mock\Tools\TestCase as TestCase; + +final class MyTestCase extends TestCase +{ + public function testMyFunction() : void + { + $mock = $this->mockStaticMethod('MyClass', 'myStaticMethod'); + $mock->expects($this->once())->willReturn('test'); + + $this->assertEquals('test', MyClass::myStaticMethod()); + } +} +``` \ No newline at end of file From c0c456c6578c1c149c58c6040a332429547ee0a0 Mon Sep 17 00:00:00 2001 From: Fulvio Notarstefano Date: Tue, 28 Mar 2023 15:13:34 +0800 Subject: [PATCH 5/7] Update README link titles --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e241389..d5235e03 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ composer require --dev 10up/wp_mock ## Documentation -Learn more about configuring and using WP_Mock by reading [the WP_Mock documentation](https://wp-mock.gitbook.io/documentation/general/introduction). +Learn more about how to configure and how to use WP_Mock by reading [the WP_Mock documentation](https://wp-mock.gitbook.io/documentation/general/introduction). ## Contributing -Please read [CODE_OF_CONDUCT.md](https://github.com/10up/wp_mock/blob/trunk/CODE_OF_CONDUCT.md) for details on our code of conduct and [CONTRIBUTING.md](https://github.com/10up/wp_mock/blob/trunk/CONTRIBUTING.md) for details on the process for submitting pull requests. +Please read our [Code of Conduct](https://github.com/10up/wp_mock/blob/trunk/CODE_OF_CONDUCT.md) for details on our code of conduct, and our [Contributing Guidelines](https://github.com/10up/wp_mock/blob/trunk/CONTRIBUTING.md) for details on the process for submitting pull requests. ## Like what you see? From c57dec02c3ddc60fa95e3d0afa753aff5fadffc5 Mon Sep 17 00:00:00 2001 From: Fulvio Notarstefano Date: Tue, 28 Mar 2023 15:20:48 +0800 Subject: [PATCH 6/7] Update Contributing.md --- CONTRIBUTING.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8201e50..e3705227 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,20 +6,23 @@ We accept contributions via Pull Requests on [Github](https://github.com/10up/wp ## Branches -* We try to follow [SemVer](http://semver.org/) in WP Mock +* WP_Mock adheres to [SemVer](http://semver.org/) (semantic versioning). * The current "stable" release version lives on the **trunk** branch. * If there is a current development release, it will live on a **{version}-dev** branch. ## Pull Requests -* New features must be submitted against the **trunk** branch +* New features must be submitted against the **trunk** branch. * Bug fixes should be submitted against the branch in which the bug exists, which is likely **trunk**. * If you're not sure whether a feature idea would be something we'd be interested in, please open an issue before you start working on it. We'd be happy to discuss your idea with you. +* Please update the **documentation** as appropriate to reflect any changes or features you have introduced in your pull request. +* Please implement appropriate **unit tests** for any code changes you are submitting in your pull request. ## Merging * As of 2019, all merges to the **trunk** branch will be squash merges of features. * If there are multiple features pending in a release, we will create a **{version}-dev** branch to track development against that version. Once the version is ready, that branch will be squash-merged into **trunk** as well. +* When a pull request is merged, the **Squash and Merge** option **must be used** when merging a pull request. ## Thanks From 6b8eaaa3f078d6088e0d3304a6189571b49fedbd Mon Sep 17 00:00:00 2001 From: Fulvio Notarstefano Date: Fri, 31 Mar 2023 11:28:20 +0800 Subject: [PATCH 7/7] update readme.md with mentioning of GitBook and contributors --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d5235e03..aff1eb19 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,12 @@ Learn more about how to configure and how to use WP_Mock by reading [the WP_Mock Please read our [Code of Conduct](https://github.com/10up/wp_mock/blob/trunk/CODE_OF_CONDUCT.md) for details on our code of conduct, and our [Contributing Guidelines](https://github.com/10up/wp_mock/blob/trunk/CONTRIBUTING.md) for details on the process for submitting pull requests. +## Supporters + +WP_Mock is supported by [10up](https://10up.com) and [GoDaddy](https://godaddy.com). [GitBook](https://www.gitbook.com/) kindly offers free hosting for [WP_Mock documentation](https://wp-mock.gitbook.io/documentation/general/introduction). + +A special thanks to all [WP_Mock contributors](https://github.com/10up/wp_mock/graphs/contributors). + ## Like what you see?