Introduction to WordPress unit testing

Introduction to WordPress unit testing

These are the slides from a talk given at WordCamp Toronto 2015.

“But this worked the other day!” We’ve all had those moments (maybe you even had one today). It’s so frustrating when things that used to work break. Sometimes, you feel a bit silly. Other times, you’re ready to flip a table. Well, put that table back down!

Let unit testing save you from this nightmare. It’s a lot like coding with a safety net (or body armor if that’s how you roll). It lets you go a bit crazy while minimizing repercussions (as long as the police don’t show up).

You can read the companion article at: https://carlalexander.ca/introduction-wordpress-unit-testing

5a4758faa5ba6c1322bdfb0f6ebcf56c?s=128

Carl Alexander

October 03, 2015
Tweet

Transcript

  1. unit testing Introduction to WordPress

  2. Carl Alexander

  3. @twigpress

  4. carlalexander.ca

  5. None
  6. This code worked the other day

  7. You need to protect yourself

  8. Say hello to testing!

  9. testing is a HUGE field

  10. Focus on unit testing

  11. What is unit testing?

  12. Testing at the smallest scale

  13. Input values into a function

  14. Check what comes out at the end

  15. Goal: Constant behaviour

  16. Goal: Safety net

  17. Goal: Code quality

  18. in unit testing Isolation

  19. Cornerstone of unit testing

  20. Done using test doubles

  21. Magical machines surrounding your code

  22. Gives you ABSOLUTE control

  23. What is my code doing?

  24. Not what is WordPress doing?

  25. What makes testable code higher quality?

  26. Cyclomatic complexity

  27. Maps unique paths through your code

  28. None
  29. Code with more paths is harder to test

  30. Makes you write smaller, less complex functions

  31. WordPress tests unit tests VS

  32. WordPress tests are integration tests

  33. Different relationship with isolation

  34. It’s about how pieces of code work together

  35. Isolating a piece of code isn’t as necessary

  36. Neither is better

  37. Ideally, you do both

  38. Questions?

  39. The unit testing setup

  40. Operating System: unix-based

  41. PHP version: 5.3 (for code) / 5.4 (for tests)

  42. Composer

  43. PHPUnit

  44. WP-CLI (optional)

  45. Creating our 
 unit tested plugin

  46. wp  scaffold  plugin  unit-­‐tested-­‐plugin

  47. Add composer.json

  48. {
        "name":  "carlalexander/unit-­‐tested-­‐plugin",
        "type":

     "project",
        "description":  "A  unit  tested  WordPress  plugin",
        "homepage":  "https://carlalexander.ca",
        "license":  "GPL-­‐3.0+",
        "require":  {
                "php":  ">=5.3.0"
        },
        "require-­‐dev":  {
                "php-­‐mock/php-­‐mock-­‐phpunit":  "^0.3"
        }
 }
  49. {
        "name":  "carlalexander/unit-­‐tested-­‐plugin",
        "type":

     "project",
        "description":  "A  unit  tested  WordPress  plugin",
        "homepage":  "https://carlalexander.ca",
        "license":  "GPL-­‐3.0+",
        "require":  {
                "php":  ">=5.3.0"
        },
        "require-­‐dev":  {
                "php-­‐mock/php-­‐mock-­‐phpunit":  "^0.3"
        }
 }
  50. {
        "name":  "carlalexander/unit-­‐tested-­‐plugin",
        "type":

     "project",
        "description":  "A  unit  tested  WordPress  plugin",
        "homepage":  "https://carlalexander.ca",
        "license":  "GPL-­‐3.0+",
        "require":  {
                "php":  ">=5.3.0"
        },
        "require-­‐dev":  {
                "php-­‐mock/php-­‐mock-­‐phpunit":  "^0.3"
        }
 }
  51. Modify bootstrap.php (created by WP-CLI)

  52. $_tests_dir  =  getenv('WP_TESTS_DIR');
 if  (  !$_tests_dir  )  $_tests_dir  =  '/tmp/wordpress-­‐tests-­‐lib';


    
 require_once  $_tests_dir  .  '/includes/functions.php';
 
 function  _manually_load_plugin()  {
   require  dirname(  __FILE__  )  .  '/../unit-­‐tested-­‐plugin.php';
 }
 tests_add_filter(  'muplugins_loaded',  '_manually_load_plugin'  );
 
 require  $_tests_dir  .  '/includes/bootstrap.php';
  53. Composer missing

  54. require_once(dirname(__DIR__)  .  '/vendor/autoload.php');
 
 $_tests_dir  =  getenv('WP_TESTS_DIR');
 if  (  !$_tests_dir

     )  $_tests_dir  =  '/tmp/wordpress-­‐tests-­‐lib';
 
 require_once  $_tests_dir  .  '/includes/functions.php';
 
 function  _manually_load_plugin()  {
   require  dirname(  __FILE__  )  .  '/../unit-­‐tested-­‐plugin.php';
 }
 tests_add_filter(  'muplugins_loaded',  '_manually_load_plugin'  );
 
 require  $_tests_dir  .  '/includes/bootstrap.php';
  55. Our first plugin function

  56. namespace  UnitTestDemo;
 /**
  *  Get  a  plugin  option  from  the

     WordPress  database.
  *
  *  @param  string  $name
  *
  *  @return  mixed
  */
 function  demo_get_option($name)
 {
        return  get_option('demo_'  .  $name);
 }  
  57. namespace  UnitTestDemo;
 /**
  *  Get  a  plugin  option  from  the

     WordPress  database.
  *
  *  @param  string  $name
  *
  *  @return  mixed
  */
 function  demo_get_option($name)
 {
        return  get_option('demo_'  .  $name);
 }  
  58. namespace  UnitTestDemo;
 /**
  *  Get  a  plugin  option  from  the

     WordPress  database.
  *
  *  @param  string  $name
  *
  *  @return  mixed
  */
 function  demo_get_option($name)
 {
        return  get_option('demo_'  .  $name);
 }  
  59. Questions?

  60. Our first unit test

  61. namespace  UnitTestDemo;
 
 use  phpmock\phpunit\PHPMock;
 
 class  DemoTest  extends  \PHPUnit_Framework_TestCase


    {
        use  PHPMock;          //  ...   }
  62. namespace  UnitTestDemo;
 
 use  phpmock\phpunit\PHPMock;
 
 class  DemoTest  extends  \PHPUnit_Framework_TestCase


    {
        use  PHPMock;          //  ...   }
  63. namespace  UnitTestDemo;
 
 use  phpmock\phpunit\PHPMock;
 
 class  DemoTest  extends  \PHPUnit_Framework_TestCase


    {
        use  PHPMock;          //  ...   }
  64. namespace  UnitTestDemo;
 
 use  phpmock\phpunit\PHPMock;
 
 class  DemoTest  extends  \PHPUnit_Framework_TestCase


    {
        use  PHPMock;          //  ...   }
  65. namespace  UnitTestDemo;
 
 use  phpmock\phpunit\PHPMock;
 
 class  DemoTest  extends  \PHPUnit_Framework_TestCase


    {
        use  PHPMock;          //  ...   }
  66. Testing demo_get_option

  67. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  68. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  69. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  70. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  71. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  72. Let's try to run it

  73. None
  74. Oops, that didn’t work! (Why is that?)

  75. Missing test double

  76. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {                  $get_option  =  $this-­‐>getFunctionMock(                          'UnitTestDemo',  'get_option'                  );   
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  77. What’s a mock?

  78. Type of test double

  79. Simulates the behaviour of a function or object

  80. Verifies how you interact with it

  81. Only type of test double that does that

  82. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {                  $get_option  =  $this-­‐>getFunctionMock(                          'UnitTestDemo',  'get_option'                  );
                $get_option-­‐>expects($this-­‐>once())
                                      -­‐>with($this-­‐>equalTo('demo_foo'))
                                      -­‐>willReturn('bar');   
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  83. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {                  $get_option  =  $this-­‐>getFunctionMock(                          'UnitTestDemo',  'get_option'                  );
                $get_option-­‐>expects($this-­‐>once())
                                      -­‐>with($this-­‐>equalTo('demo_foo'))
                                      -­‐>willReturn('bar');   
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  84. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {                  $get_option  =  $this-­‐>getFunctionMock(                          'UnitTestDemo',  'get_option'                  );
                $get_option-­‐>expects($this-­‐>once())
                                      -­‐>with($this-­‐>equalTo('demo_foo'))
                                      -­‐>willReturn('bar');   
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  85. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {                  $get_option  =  $this-­‐>getFunctionMock(                          'UnitTestDemo',  'get_option'                  );
                $get_option-­‐>expects($this-­‐>once())
                                      -­‐>with($this-­‐>equalTo('demo_foo'))
                                      -­‐>willReturn('bar');   
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  86. None
  87. Our first passing test!
 (Yay!)

  88. Questions?

  89. Pushing things further

  90. Common to store an array inside an option

  91. Except you might not always get an array back

  92. You’ll often see (array) next to get_option

  93. Let’s make our function do this for us

  94. But let’s do it in reverse
 (plot twist!)

  95. Start with a failing test

  96. Gives you a taste for test-driven development

  97. First, a small change to demo_get_option

  98. namespace  UnitTestDemo;
 /**
  *  Get  a  plugin  option  from  the

     WordPress  database.
  *
  *  @param  string  $name
  *  @param  mixed    $default
  *
  *  @return  mixed
  */
 function  demo_get_option($name,  $default  =  null)
 {
        return  get_option('demo_'  .  $name,  $default);
 }  
  99. Update our test as well

  100. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option()
        {                  $get_option  =  $this-­‐>getFunctionMock(                          'UnitTestDemo',  'get_option'                  );
                $get_option-­‐>expects($this-­‐>once())
                                      -­‐>with(                                                      $this-­‐>equalTo('demo_foo'),                                                      $this-­‐>identicalTo(null)                                        )
                                      -­‐>willReturn('bar');   
                $this-­‐>assertEquals('bar',  demo_get_option('foo'));
        }   }
  101. identicalTo() equalTo() VS

  102. Creating our failing test

  103. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option_casts_array()
        {                  $get_option  =  $this-­‐>getFunctionMock(                          'UnitTestDemo',  'get_option'                  );
                $get_option-­‐>expects($this-­‐>once())
                                      -­‐>with(                                                      $this-­‐>equalTo('demo_foo'),                                                      $this-­‐>identicalTo(array())                                        )
                                      -­‐>willReturn('bar');   
                $this-­‐>assertEquals(
                        array('bar'),  demo_get_option('foo',  array())
                );
        }   }
  104. Similar to our other test

  105. Changing inputs, outputs and expected values

  106. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option_casts_array()
        {                  $get_option  =  $this-­‐>getFunctionMock(                          'UnitTestDemo',  'get_option'                  );
                $get_option-­‐>expects($this-­‐>once())
                                      -­‐>with(                                                      $this-­‐>equalTo('demo_foo'),                                                      $this-­‐>identicalTo(array())                                        )
                                      -­‐>willReturn('bar');   
                $this-­‐>assertEquals(
                        array('bar'),  demo_get_option('foo',  array())
                );
        }   }
  107. class  DemoTest  extends  \PHPUnit_Framework_TestCase
 {
        public  function

     test_demo_get_option_casts_array()
        {                  $get_option  =  $this-­‐>getFunctionMock(                          'UnitTestDemo',  'get_option'                  );
                $get_option-­‐>expects($this-­‐>once())
                                      -­‐>with(                                                      $this-­‐>equalTo('demo_foo'),                                                      $this-­‐>identicalTo(array())                                        )
                                      -­‐>willReturn('bar');   
                $this-­‐>assertEquals(
                        array('bar'),  demo_get_option('foo',  array())
                );
        }   }
  108. None
  109. Let’s get the test to pass

  110. namespace  UnitTestDemo;
 
 /**
  *  Get  a  plugin  option  from

     the  WordPress  database.
  *
  *  @param  string  $name
  *  @param  mixed    $default
  *
  *  @return  mixed
  */
 function  demo_get_option($name,  $default  =  null)
 {
        $option  =  get_option('demo_'  .  $name,  $default);
 
        if  (is_array($default)  &&  !is_array($option))  {
                $option  =  (array)  $option;
        }
 
        return  $option;
 }
  111. None
  112. And there you go!

  113. Questions?

  114. Building your testing habit

  115. A test, by itself, feels pretty insignificant

  116. Just one loop in your safety net

  117. You need more than one for them to support you

  118. Testing is a lot like picking up a new habit

  119. Hard to stay motivated (Chips are soooo tasty!)

  120. https://github.com/carlalexander/wordpress-unit-test-demo Ready to start?

  121. Thank you!