Upgrade to Pro — share decks privately, control downloads, hide ads and more …

WordCamp Talk: Writing Testable Plugins

WordCamp Talk: Writing Testable Plugins

My talk for WordCamp Boston 2016

Payton Swick

July 23, 2016
Tweet

Other Decks in Programming

Transcript

  1. WRITING TESTABLE PLUGINS? WHY? These days the systems we are

    building are more and more complex. We press one button and get a result, but a million things happen behind the scenes. If something goes wrong, you don't want to get one of those "Sorry, an error occurred!" messages. You want to know exactly what happened. Good unit tests will give you that.
  2. WRITING TESTABLE PLUGINS? WHY? Unit tests allow us to design

    the system we are building before we build it. As we build it we will probably want to change things. Good unit tests allow us to safely change our code without breaking anything that came before.
  3. WHAT IS A UNIT TEST? Some code to test that

    one piece of a system operates as expected.
  4. Database tests are a big unit. Function tests are a

    small unit. Focus on the small.
  5. Also, WordPress is a big unit! It's nice to be

    able to test a plugin without WordPress.
  6. WHAT IS A UNIT TEST? (PT. 2) Unit tests check

    the output for a given input. ! !
  7. WHAT IS TESTABLE CODE? Code that makes it easy to

    test inputs and outputs in isolation.
  8. To reduce dependencies: 1. Pass input to your functions rather

    than having your function fetch input themselves. 2. Put dependencies in variables that can be overridden.
  9. " Nope. function update_post_status( $post_id ) { $post = get_post(

    $post_id ); $post->post_status = 'publish'; return wp_update_post( $post ); } update_post_status( 123 ); # Yay! function update_post_status( $post ) { $post->post_status = 'publish'; return wp_update_post( $post ); } update_post_status( get_post( 123 ) );
  10. " Nope. class MyPlugin { function update_post_status( $post_id ) {

    $updater = new Updater(); $updater->update_post( $post_id ); } } # Yay! class MyPlugin { public $updater; public function __construct( $updater = null ) { $this->updater = $updater ? $updater : new Updater(); } public function update_post_status( $post_id ) { $this->updater->update_post( $post_id ); } }
  11. Allowing dependencies to be overwritten is called "Dependency Injection". Tests

    can isolate one part of code from another by replacing the dependencies.
  12. To reduce side effects: 1. Have each function do just

    one thing. 2. Make more (specific) classes and functions.
  13. " Nope. function update_post_data( $post ) { $post->post_status = 'publish';

    $post->post_excerpt = substr( $post->post_content, 0, 140 ); return wp_update_post( $post ); } update_post_data( get_post( 123 ) ); # Yay! function update_post_status( $post ) { $post->post_status = 'publish'; return $post; } function update_post_excerpt( $post ) { $post->post_excerpt = substr( $post->post_content, 0, 140 ); return $post; } $post = update_post_status( get_post( 123 ) ); $post = update_post_excerpt( $post ); wp_update_post( $post );
  14. " Nope. function otherpages_add_shortcode() { ... } # Yay! class

    Otherpages { public function add_shortcode() { ... } }
  15. There are lots of ways to make mock objects and

    functions. but I'll suggest a library called Spies. https://github.com/sirbrillig/spies (I wrote it.)
  16. Code getting input from a function dependency: function get_the_content( $id

    ) { $post = get_post( $id ); return $post->post_content; } A test of that code with a mock function dependency: \Spies\mock_function( 'get_post' )->that_returns( (object) [ 'ID' => 123, 'post_content' => 'hello' ] ); $result = get_the_content( 123 ); $this->assertEquals( 'hello', $result );
  17. Code getting input from an object dependency: class MyPlugin {

    public $getter; public function __construct( $getter = null ) { $this->getter = $getter ? $getter : new Getter(); } public function get_data_content() { $post = $this->getter->get_post(); return $post->post_content; } } A test of that code with a mock object dependency: $getter = \Spies\mock_object(); $getter->add_method( 'get_post' )->that_returns( (object) [ 'ID' => 123, 'post_content' => 'hello' ] ); $plugin = new MyPlugin( $getter ); $result = $plugin->get_data_content(); $this->assertEquals( 'hello', $result );
  18. Generators are a shortcut for object creation. Static Generators are

    ok. class MyPlugin { public $getter; public function __construct( $getter ) { $this->getter = $getter; } public static function get_instance() { return new MyPlugin( new Getter() ); } } They don't always need to be tested because we can test the object creation manually.
  19. Static functions that have no dependencies and no side-effects are

    ok. class MyPlugin { public static function is_published_post( $post ) { return $post->post_status === 'publish'; } } These can be tested without mocks.
  20. " Nope. define( 'DATA_DIR', '/data/dir' ); class MyPlugin { public

    function get_data() { return file_get_contents( DATA_DIR . '/data.txt' ); } } # Yay! class MyPlugin { public $data_dir = '/data/dir'; public function get_data() { return file_get_contents( $this->data_dir. '/data.txt' ); } }
  21. Sometimes you need to get data from one place and

    have something else pick it up later. % & ' & &
  22. Here's an example of using a filter. function read_data( $post

    ) { add_filter( 'my_data', function() use ( &$post ) { return $post->post_content; } ); } function return_data() { return apply_filters( 'my_data', $post->post_content ); } You can write tests to be sure the filter is added and applied. mock_function( 'apply_filters' )-> when_called->with( 'my_data' )->will_return( 'foobar' ); $this->assertEquals( 'foobar', return_data() );
  23. A function name should tell you what inputs and outputs

    are expected. Some great verbs are get, is, update, create, remove.
  24. Example: a shortcode handler add_shortcode( 'otherpages', ... ); ) my_shortcode

    doesn't really say what it does. process_my_shortcode is better, but still ambiguous about what the function returns. get_markup_from_shortcode tells us what the input and output will be.
  25. " Nope. function get_recent_post_titles( $posts ) { $matching = [];

    foreach( $posts as $post ) { if ( time() - strtotime( $post->post_date_gmt ) < 86400 ) { $matching[] = $post->post_title; } } return $matching; } # Yay! function get_recent_post_titles( $posts ) { $posts = array_filter( $posts, 'is_post_recent' ); return array_map( 'get_post_title', $posts ); } function is_post_recent( $post ) { return ( time() - strtotime( $post->post_date_gmt ) < 86400 ); } function get_post_title( $post ) { return $post->post_title; }
  26. " Nope. function get_post_title( $post_id ) { $post = get_post(

    $post_id ); return $post->post_title; } # Yay! function get_post_title( $post_id ) { $post = get_post( $post_id ); if ( $post ) { return $post->post_title; } return ''; }
  27. Tested code gives us confidence. It will work the way

    we expect. (We have defined our expectations.) It can be refactored safely.
  28. We can assume that WordPress works. (Otherwise we have bigger

    problems!) But we should make sure that we are using WordPress the way we expect.
  29. WHAT IS A SPY? An object that behaves like a

    function and which we can query to learn how it was called.
  30. (do a thing that calls a method) "Hey spy! Did

    that method get called the way we expected?"
  31. Here we use a spy to make sure a shortcode

    is added correctly. function add_my_shortcode() { add_shortcode( 'otherpages', 'get_markup_from_shortcode' ); } $spy = \Spies\get_spy_for( 'add_shortcode' ); add_my_shortcode(); $this->assertTrue( $spy->was_called_with( 'otherpages', \Spies\any() ); );
  32. A unit is one thing. A unit test should test

    that one thing. One function's one input and one output.
  33. "Unit tests isolate failures. Even if a product contains millions

    of lines of code, if a unit test fails, you only need to search that small unit under test to find the bug." https://googletesting.blogspot.co.uk/2015/04/just-say-no-to-more-end-to-end-tests.html
  34. Code is hard. You can't predict all possible situations. When

    you find a bug, write a test that causes it, and then fix the bug so the test passes.
  35. WHAT IS A UNIT TEST? (PT. 3) Confidence, safety, readability.

    An investment in the future of your code.
  36. * * * Big thanks to the WordPress.com REST API

    test suite, and all its overworked, under-appreciated maintainers. And big thanks to you for coming!