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

    View Slide

  2. WHO AM I?
    Payton Swick
    Code Wrangler at Automattic
    (we make WordPress.com)
    @sirbrillig

    View Slide

  3. 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.

    View Slide

  4. 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.

    View Slide

  5. WHAT IS A UNIT TEST?
    Some code to test that one piece of a system operates as
    expected.

    View Slide

  6. UNIT?
    There are units of different sizes.

    View Slide

  7. Database tests are a big unit. Function tests are a small unit.
    Focus on the small.

    View Slide

  8. Smaller systems are easier to understand.

    View Slide

  9. Also, WordPress is a big unit!
    It's nice to be able to test a plugin
    without WordPress.

    View Slide

  10. NOTE
    You might not need unit tests!
    (But it can't hurt!)

    View Slide

  11. WHAT IS A UNIT TEST? (PT. 2)
    Unit tests check the output for a given input.
    ! !

    View Slide

  12. WHAT IS TESTABLE CODE?
    Code that makes it easy to test
    inputs and outputs in isolation.

    View Slide

  13. Isolation is the hard part.
    Most code has dependencies and side effects.

    View Slide

  14. Dependencies:
    this code's input is not just in the arguments.

    View Slide

  15. 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.

    View Slide

  16. " 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 ) );

    View Slide

  17. " 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 );
    }
    }

    View Slide

  18. Allowing dependencies to be overwritten is called
    "Dependency Injection".
    Tests can isolate one part of code from another by replacing
    the dependencies.

    View Slide

  19. Side Effects:
    this code's output is not just in its return value.

    View Slide

  20. To reduce side effects:
    1. Have each function do just one thing.
    2. Make more (specific) classes and functions.

    View Slide

  21. " 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 );

    View Slide

  22. PAYTON'S TESTABLE PRECEPTS

    View Slide

  23. Always use classes, never global functions.
    Precept 1:

    View Slide

  24. " Nope.
    function otherpages_add_shortcode() {
    ...
    }
    # Yay!
    class Otherpages {
    public function add_shortcode() {
    ...
    }
    }

    View Slide

  25. One class to do one thing.
    Don't be afraid of too many classes.

    View Slide

  26. Where two classes interact, we can place a mock.

    View Slide

  27. MOCKS (AKA STUBS):
    fake objects or functions that behave in a way we can
    control.

    View Slide

  28. 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.)

    View Slide

  29. 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 );

    View Slide

  30. 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 );

    View Slide

  31. Don't use static functions except as generators.
    Precept 2:

    View Slide

  32. Why are static methods risky?
    They cannot easily be mocked.
    $

    View Slide

  33. 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.

    View Slide

  34. 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.

    View Slide

  35. Use instance variables as constants.
    Precept 3:

    View Slide

  36. Instance variables can be changed during runtime to
    produce different results for testing.

    View Slide

  37. " 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' );
    }
    }

    View Slide

  38. Use filters to pass data indirectly.
    Precept 4:

    View Slide

  39. Better to pass data directly, but...
    % & & & &

    View Slide

  40. Sometimes you need to get data from one place and have
    something else pick it up later.
    % & ' & &

    View Slide

  41. When passing data between functions indirectly, use a filter.
    Filters can be mocked.
    (Thanks WordPress!)

    View Slide

  42. 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() );

    View Slide

  43. Use verbs in all function names.
    Precept 5:

    View Slide

  44. Isn't that just style?
    (

    View Slide

  45. A function name should tell you what inputs and outputs are
    expected.
    Some great verbs are get, is, update, create, remove.

    View Slide

  46. 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.

    View Slide

  47. If a function does too many things to name, consider
    splitting it.

    View Slide

  48. Keep functions below eight lines
    and indentation below four levels.
    Precept 6:

    View Slide

  49. Shorter functions are easier to understand.

    View Slide

  50. Use array_map, array_filter, array_reduce, etc.
    instead of while, for, and foreach.

    View Slide

  51. " 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;
    }

    View Slide

  52. Consider all possible inputs and outputs.
    Precept 7:

    View Slide

  53. Check for errors from external functions.

    View Slide

  54. When testing (or in real life!), your data might be missing or
    incomplete.

    View Slide

  55. " 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 '';
    }

    View Slide

  56. Whenever possible, write tests first.
    Precept 8:

    View Slide

  57. Tested code gives us confidence.
    It will work the way we expect.
    (We have defined our expectations.)
    It can be refactored safely.

    View Slide

  58. Don't test the code from other libraries,
    Precept 9:
    but test the inputs and outputs.

    View Slide

  59. 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.

    View Slide

  60. We can test the inputs to external functions with spies.

    View Slide

  61. WHAT IS A SPY?
    An object that behaves like a function and which we can
    query to learn how it was called.

    View Slide

  62. (do a thing that calls a method)
    "Hey spy! Did that method get called the way we expected?"

    View Slide

  63. 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()
    );
    );

    View Slide

  64. Write only one assertion per test.
    Precept 10:

    View Slide

  65. A unit is one thing.
    A unit test should test that one thing.
    One function's one input and one output.

    View Slide

  66. Can a function have more than one input or output?
    Write more than one test.

    View Slide

  67. "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

    View Slide

  68. 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.

    View Slide

  69. WHAT IS A UNIT TEST? (PT. 3)
    Confidence, safety, readability.
    An investment in the future of your code.

    View Slide

  70. * * *
    Big thanks to the WordPress.com REST API test suite, and all
    its overworked, under-appreciated maintainers.
    And big thanks to you for coming!

    View Slide

  71. +
    A simple shortcode plugin with tests:
    https://github.com/sirbrillig/otherpages
    Spies:
    https://github.com/sirbrillig/spies
    #
    Good testing!

    View Slide