Slide 1

Slide 1 text

Testing WordPress… ...without WordPress Giuseppe Mazzapica

Slide 2

Slide 2 text

Unit tests are a simple thing. Take the smallest portion of code that can be extracted from the rest of the application, execute it, and get feedback about the obtained result being the expected result.

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Slide 5

Slide 5 text

( ); $iterator append 'test' ( ) === ; return count $iterator 1 } ); $ php my-tests.php It should sum two numbers. ✔ ✘ It should display an X for a failing test. FAIL in: /path/to/my-tests.php #6 ✔ It should append to an ArrayIterator. 1 2 3 4 5 6 7 8 9 10 11 12

Slide 6

Slide 6 text

namespace MyCompany\MyPlugin; final class { Email private ; $email public function ( string ) { __construct $email if ( ! ( , ) ) { filter_var $email FILTER_VALIDATE_EMAIL ( { } ); throw new \Exception " isn’t valid." $email } -> = ; $this email $email } public function (): string { __toString -> ; return $this email } public function ( Email ): bool { equals $email (string) === (string) ; return $this $email } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

Slide 7

Slide 7 text

require_once ; '/path/to/test-framework-in-a-tweet.php' require_once ; '/path/to/code/src/MyCompany/MyPlugin/Email.php' ( , function () { it 'should equal its string value.' = ( ); $email new MyCompany\MyPlugin\Email '[email protected]' (string) === ; $email return '[email protected]' } ); ( , function () { it 'should equal another instance by value.' = ( ); $email new MyCompany\MyPlugin\Email '[email protected]' -> ( new ( ) ); $email return equals Email '[email protected]' } ); 1 2 3 4 5 6 7 8 9 10 11 12 13 $ php my-tests.php It should equal its string value. ✔ It should equal another instance by value. ✔

Slide 8

Slide 8 text

We tested our class with a tweet-sized function Our function is part of a WordPress plugin We tested our class without loading WordPress

Slide 9

Slide 9 text

but what about... namespace MyCompany\MyPlugin; function () { register_product_cpt ( , [ ] ); register_post_type 'product' /* bunch of args... */ } 1 2 3 4 5 Can we test this function without loading WordPress?

Slide 10

Slide 10 text

YES, WE CAN. but what about... namespace MyCompany\MyPlugin; function () { register_product_cpt ( , [ ] ); register_post_type 'product' /* bunch of args... */ } 1 2 3 4 5 Can we test this function without loading WordPress?

Slide 11

Slide 11 text

Slide 12

Slide 12 text

register_post_type works post_type_exists works our function calls WordPress functions in the proper way What is that test testing?

Slide 13

Slide 13 text

Our responsibility, when writing tests for a plugin, is to test code works. our The best way to do it, is to write tests assuming WP just works.

Slide 14

Slide 14 text

function ( ) { post_type_exists $post_type ( { } , ); return array_key_exists "post_type_ " $post_type $GLOBALS } function ( , ) { register_post_type $post_type $args [ { } ] = ; $GLOBALS "post_type_ " $post_type $args } it( , function () { 'should register product post type.' = ( ); $exists_before post_type_exists 'product' (); MyCompany\MyPlugin\register_product_cpt = ( ); $exists_after post_type_exists 'product' === false && = true; return $exists_before $exists_after } ); 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

Slide 15

Slide 15 text

Test code did not changed. But this time we didn’t tested WP code works, but we tested that our code calls WordPress functions in the proper way. And it was easy and fast.

Slide 16

Slide 16 text

Because WordPress was not loaded, we were able to write a “fake”, simplified, version of WordPress functions, for the sole purpose of testing. That’s called . STUB

Slide 17

Slide 17 text

It was . convenient Without loading WordPress we: avoided scaffolding step removed the burden of database and system configuration improved tests performance

Slide 18

Slide 18 text

To run tests in the context of WordPress is important at some point to verify the correctness of the plugin. It is essential for plugins with heavy UI integration.

Slide 19

Slide 19 text

What about objects? 3 4 5 6 7 8 9 10 11 12 13 14 15 16 interface { Clock public function (): int; hours public function (): int; minutes public function (): int; seconds } function ( Clock ) { maybeGong $clock if ( -> () === && -> () === ) { $clock $clock minutes seconds 0 0 = -> (); $hours $clock hours = > && <= ? : ( - ); $gongs $h $h $h $h 0 12 12 abs ( ( , ) ); print rtrim str_repeat 'GONG ' $gongs } }

Slide 20

Slide 20 text

3 4 5 6 7 8 9 10 11 12 13 14 15 16 final class implements { SystemClock Clock public function (): int { hours (int) ( () )-> ( ); return new \Datetime format 'G' } public function (): int { minutes (int) ( () )-> ( ); return new \Datetime format 'i' } public function (): int { seconds (int) ( () )-> ( ); return new \Datetime format 's' } } A stub will allow us to test this non-determistic behavior.

Slide 21

Slide 21 text

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class implements { ClockStub Clock private ; $hours private ; $minutes public function ( int , int ) { __construct $hours $minutes -> = ; $this hours $hours -> = ; $this minutes $minutes } public function (): int { hours -> ; return $this hours } public function (): int { minutes -> ; return $this minutes } public function (): int { seconds ; return 0 } }

Slide 22

Slide 22 text

3 4 5 6 7 8 9 10 11 12 13 14 it( , function () { "should gong 3 times at 3 o'clock." (); ob_start ( ( , ) ); maybeGong new ClockStub 3 0 () === ; return ob_get_clean "GONG GONG GONG" } ); ( , function () { it 'should not gong at 10 past 3.' (); ob_start ( ( , ) ); maybeGong new ClockStub 3 10 () === ; return ob_get_clean "" } );

Slide 23

Slide 23 text

We leveraged polymorphism to write a stub object that is a different, deterministic, implementation of the same interface. We could do that because the maybe_gong() function was written to accept an interface as parameter.

Slide 24

Slide 24 text

Stubs are great, but stubs are . hard Stubs can be written for . objects Stubs can be written for . functions

Slide 25

Slide 25 text

Stubs are . code And code needs to be written. Code needs to be maintained. Code might have has bugs.

Slide 26

Slide 26 text

Introducing mock objects. A sort of virtual stubs: they are not written as real classes in the filesystem, but created on runtime based on a set of expectations.

Slide 27

Slide 27 text

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 it( , function () { "should gong 3 times at 3 o'clock." = :: ( ); $threeOClock Mockery mock 'Clock' -> ( )-> () $threeOClock shouldReceive once 'hours' -> ()-> ( ); withNoArguments andReturn 3 -> ( )-> () $threeOClock shouldReceive once 'minutes' -> ()-> ( ); withNoArguments andReturn 0 -> ( )-> () $threeOClock shouldReceive once 'seconds' -> ()-> ( ); withNoArguments andReturn 0 (); ob_start ( ); $threeOClock maybeGong = () === ; $result ob_get_clean "GONG GONG GONG" :: (); Mockery close ; $result return });

Slide 28

Slide 28 text

Mock Objects respond to method calls in a pre-defined way (like stubs) ensure methods are called a specific number of times with specific type and number of args offer less surface to bugs, moving the complexity to a 3rd party lib

Slide 29

Slide 29 text

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class { ProductsQuery private ; $query private = ; $paged 0 public function ( = NULL ) { __construct WP_Query $query -> = ? : (); $this query $query new WP_Query } public function (): array { next_products -> ++; $this paged -> -> ( [ $this query query => , 'post_type' 'product' => , 'posts_per_page' 10 => -> , $this 'paged' paged ] ); if ( ! -> -> () ) { $this query have_posts []; return } -> -> ; $this return query posts }

Slide 30

Slide 30 text

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 it( , function () { "should get first 10 products." = ( function() { $ten_posts array_map :: ( ); return Mockery mock 'WP_Post' }, ( , ) ); range 0 9 = :: ( ); $query_mock Mockery mock 'WP_Query' $query_mock -> ( )-> () shouldReceive once 'query' -> ( function ( array ) { withArgs $args ( [ ] ?? ) === ; return $args 'post_type' '' 'product' } ) -> ( , ); andSet 'posts' $ten_posts $query_mock -> ( )-> () shouldReceive once 'have_posts' -> ()-> ( true ); withNoArgs andReturn = ( ); $query $query_mock new ProductsQuery -> () === ; return $query $ten_posts next_products } );

Slide 31

Slide 31 text

When that is done, that fact that actual posts are pulled from DB is not a concern to test in our plugin. Thanks to Mockery we tested that WP_Query methods are called the right number of times, with the right arguments.

Slide 32

Slide 32 text

Set expectations for functions Define functions multiple times What is necessary to obtain for functions the same power Mockery provides for objects?

Slide 33

Slide 33 text

Set expectations for functions Define functions multiple times > Doable with a little bit of code > Doable thanks to Patchwork What is necessary to obtain for functions the same power Mockery provides for objects?

Slide 34

Slide 34 text

Brain Monkey is a library that uses Patchwork to bring Mockery object expectations to functions.

Slide 35

Slide 35 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 () . ; ' ' "\n" $t getMessage } } Just a little of configuration

Slide 36

Slide 36 text

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function ( ) { last_post_shortcode $args = [ => 1, => 0, => ]; $def 'date' 'content' 'post_type' 'post' = ( , , ); $atts $def shortcode_atts $args 'last_post' = ([ $posts get_posts => [ ], $atts 'post_type' 'post_type' => , 'posts_per_page' 1 ]); if ( ) { $posts [ ] = ( ); $GLOBALS $posts 'post' reset ( [ ]); $GLOBALS setup_postdata 'post' = ( , ()); $out sprintf '
' get_the_ID if ( [ ]) { $atts 'date' = ( ()); $date esc_html get_the_date .= ( , ); $out $date sprintf '

%s

' } .= ( , ( ())); $out sprintf '

%s

' esc_html get_the_title [ ] and .= (); $atts $out 'content' get_the_content (); wp_reset_postdata . ; $out return '
' } }

Slide 37

Slide 37 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 true, => ]; $args 'content' 'post_type' 'products' ( ) Brain\Monkey\Functions\expect 'get_posts' -> () once -> ( function( array ) { andReturnUsing $args if ( [ ] === ) { $args 'post_type' 'products' [ :: ( ) ]; return Mockery mock 'WP_Post' } }); ( ) Brain\Monkey\Functions\expect 'shortcode_atts' -> () once -> ( :: ( ), , ) with type Mockery 'array' 'lastpost' $args -> ( function( , array ) { andReturnUsing $defaults $args ( , ); return array_merge $defaults $args } );

Slide 38

Slide 38 text

1 2 3 4 5 6 .. 23 24 25 26 27 28 29 30 31 () once -> ( :: ( ) ); with Mockery type 'WP_Post' ( ) Brain\Monkey\Functions\expect 'wp_reset_postdata' -> () once -> (); withNoArgs

Slide 39

Slide 39 text

1 2 3 4 5 6 .. 31 32 33 34 35 36 37 38 39 , 'get_the_ID' 123 => , 'get_the_date' '15/12/2017' => , 'get_the_title' 'Test Title' => , 'get_the_content' '

The content!

' => null, 'esc_html' ] );

Slide 40

Slide 40 text

1 2 3 4 5 6 7 8 .. 39 40 41 42 43 44 45 46 47 48 true, => ]; $args 'content' 'post_type' 'products' ... = ( ); $rendered $args last_post_shortcode = ( , ); $has_id $rendered strpos 'id="123"' = ( , ); $has_date $rendered strpos '15/12/2017

' = ( , ); $has_title $rendered strpos '

Test Title

' = ( , ); $has_content $rendered strpos '

The content!

' && && && ; return $has_id $has_date $has_title $has_content }

Slide 41

Slide 41 text

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 bm\ ( , function () { it 'should render shortcode for last product.' = [ => TRUE, => ]; $args 'content' 'post_type' 'products' \ ( ) Brain\Monkey\Functions expect 'get_posts' -> () once -> ( function ( array ) { andReturnUsing $args if ( [ ] === ) { $args 'post_type' 'products' [ \ :: ( ) ]; return Mockery mock 'WP_Post' } } ); \ ( ) Brain\Monkey\Functions expect 'shortcode_atts' -> () once -> ( \ :: ( ), \ :: ( ), ) with type type Mockery Mockery 'array' 'array' 'last_post' -> ( function ( , array ) { andReturnUsing $defaults $args ( , ); return array_merge $defaults $args } ); \ ( ) Brain\Monkey\Functions expect 'setup_postdata' -> () once -> ( :: ( ) ); with type Mockery 'WP_Post' \ ( ) Brain\Monkey\Functions expect 'wp_reset_postdata' -> () once -> (); withNoArgs \ ( [ Brain\Monkey\Functions stubs => , 'get_the_ID' 123 => , 'get_the_date' '15/12/2017' => , 'get_the_title' 'Test Title' => null, 'esc_html' => , 'get_the_content' '

The content!

' ] ); = ( ); $rendered $args last_post_shortcode = ( , ); $has_id $rendered strpos 'id="123"' = ( , ); $has_date $rendered strpos '15/12/2017

' = ( , ); $has_title $rendered strpos '

Test Title

' = ( , ); $has_content $rendered strpos '

The content!

' && && && ; $has_id $has_date $has_title $has_content return } ); Quite a lot of code... but the limited features of our test framework do not help here...

Slide 42

Slide 42 text

There is a set of functions that we find in each WordPress plugins, and mock them every time is quite tedious and awkward: plugin API. add_filter() apply_filters() add_action() do_action() current_filter() did_action() and others...

Slide 43

Slide 43 text

Brain Monkey defines already almost all those functions in a way that is 100% compatible with WordPress real code.

Slide 44

Slide 44 text

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

Slide 45

Slide 45 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Slide 46

Slide 46 text

Thanks to Brain Monkey we ran our tests just like WordPress was loaded, without any mock or stub, and everything worked as expected. But there’s more...

Slide 47

Slide 47 text

1 2 3 4 5 6 7 8 9 10 11 12 bm\ ( it , 'should not fire my_plugin_init if WP is not initialized.' function () { \ ( ) 'my_plugin_init' Brain\Monkey\Actions expectDone -> (); never (); MyPlugin\create_initializer TRUE; return } );

Slide 48

Slide 48 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bm\ ( it , 'should fire my_plugin_init once if WP is initialized.' function () { ( ) 'my_plugin_init' Brain\Monkey\Actions\expectDone -> () once -> (); withNoArgs ( ); 'init' do_action (); MyPlugin\create_initializer TRUE; return } );

Slide 49

Slide 49 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bm\ ( it , 'should add hook initializer method to template redirect' function () { ( ) 'template_redirect' Brain\Actions\expectAdded -> () once -> ( function ( array ) { withArgs $cb return ( [ ], ::class ) is_a $cb 0 MyPlugin\Initializer && [ ] === ; 'init' $cb 1 } ); ( ); 'init' do_action (); MyPlugin\create_initializer true; return } );

Slide 50

Slide 50 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bm\ ( it , 'should use a different initializer class if filtered.' function () { = :: ( ::class ); $mock Mockery MyPlugin\Initializer mock = ( ); $mock_class $mock get_class ( ) 'my_plugin_init_class' Filters\expectApplied -> () once -> ( ::class ) with MyPlugin\Initializer -> ( ); andReturn $mock_class ( ); 'init' do_action return ( (), ); is_a MyPlugin\create_initializer $mock_class } );

Slide 51

Slide 51 text

Brain Monkey allows to test plugin API functions with the finest level of control, and with a syntax that is readable and consistent with the one used to create mock objects with Mockery.

Slide 52

Slide 52 text

Brain Monkey defines following logicless functions: __return_true() __return_false() __return_null() __return_empty_array() __return_empty_string() __return_zero() trailingslashit() untrailingslashit()

Slide 53

Slide 53 text

Thanks to Mockery and Brain Monkey we have the possibility to write unit tests for plugins without loading WordPress in a way that is very easy to start with and produces maintainable tests.

Slide 54

Slide 54 text

Unit tests are a simple concept Do not load WP for plugins unit tests is often convenient Mockery and Brain Monkey can help with that. Takeaways:

Slide 55

Slide 55 text

Giuseppe Mazzapica Biggest German WordPress Agency WordPress.com Featured Service Partner Gold Certified WooCommerce Expert https://gmazzap.me @gmazzap github.com/gmazzap UESTIONS Q ?