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 full-size slide

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

    View full-size 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 full-size 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 full-size slide

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

    View full-size slide

  6. UNIT?
    There are units of different sizes.

    View full-size slide

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

    View full-size slide

  8. Smaller systems are easier to understand.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

  22. PAYTON'S TESTABLE PRECEPTS

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size slide

  35. Use instance variables as constants.
    Precept 3:

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

  44. Isn't that just style?
    (

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  49. Shorter functions are easier to understand.

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

  53. Check for errors from external functions.

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide