WordPress + Unit Testing

Mike Van Winkle 

www.mikevanwinkle.com   ~  @mpvanwinkle

Mike Van Winkle

  • Sonoma County, CA
  • WordPress Developer Since 2008
  • Plugins: Simplr Registration Form, Twigify, Yamlfy
  • Pantheon.io

Unit 

Testing

testing a 'unit' of work

testing a function

testing object method

PHPUNIT

All languages have libraries that help them unit test. PHP has PHPUnit. 

 

curl -o /usr/local/bin/phpunit \
https://phar.phpunit.de/phpunit.phar

chmod +x /usr/local/bin/phpunit

phpunit 
<?php
// returns the 'dog' from an input array
function get_dog_from_cat( $cat ) {
    if ( is_array($cat) 
          AND array_key_exists('dog') ) {
        return $cat['dog'];
    }
    return false;
}
<?php
class TestSomeFunctions extends PHPUNIT_Framework_TestCase {

    function testDogFromCat() {
        $test = array( 
            'cat' => 'Mabel',
            'cat' => 'Tommy',
            'dog' => 'Mike',
        );
        $this->assertEquals( 'mike', get_dog_from_cat($test) ); 
    }
    
}

Run PHPUNIt

$> phpunit tests/test-test.php
PHPUnit 4.0.20 by Sebastian Bergmann.

E

Time: 49 ms, Memory: 2.50Mb

There was 1 error:

1) SomeFunctions::testDogFromCat
array_key_exists() expects exactly 2 parameters, 1 given

/srv/www/mikevanwinkle/tests/functions.php:5
/srv/www/mikevanwinkle/tests/test-test.php:11

FAILURES!
Tests: 1, Assertions: 0, Errors: 1.
vagrant@vvv:/srv/www/mikevanwinkle$

Success!

$> phpunit tests/test-test.php

PHPUnit 4.0.20 by Sebastian Bergmann.

.

Time: 31 ms, Memory: 2.75Mb

OK (1 test, 1 assertion)

Common Assertions

  • assertTrue()
  • assertFalse()
  • assertGreaterThan()
  • assertGreaterThanOrEqual()
  • assertArrayHasKey()
  • assertInstanceOf()
  • assertObjectHasAttribute()

PreRequisites

WP-CLI

Dev Environment

Brain

Install WP Unit Testing Suite

Before

./src
./vendor
composer.json
meta_boxes.yaml
post_types_config.yaml
README.md
readme.txt
yaml-post-types.php
./bin
./src
./tests
./vendor
composer.json
meta_boxes.yaml
phpunit.xml
post_types_config.yaml
README.md
readme.txt
yaml-post-types.php

After

$> wp scaffold plugin-tests plugin-name

Install WP Unit Testing Suite

<phpunit
	bootstrap="tests/bootstrap.php"
	backupGlobals="false"
	colors="true"
	convertErrorsToExceptions="true"
	convertNoticesToExceptions="true"
	convertWarningsToExceptions="true"
	>
	<testsuites>
		<testsuite>
			<directory prefix="test-" suffix=".php">./tests/</directory>
		</testsuite>
	</testsuites>
</phpunit>
phpunit.xml

Install WP Unit Testing Suite

$ bash bin/install-wp-tests.sh testdb testuser testpass
+ install_wp
+ mkdir -p /tmp/wordpress/
+ '[' latest == latest ']'
+ local ARCHIVE_NAME=latest
+ wget -nv -O /tmp/wordpress.tar.gz https://wordpress.org/latest.tar.gz
2015-03-25 15:58:18 URL:https://wordpress.org/latest.tar.gz [6186275/6186275] -> "/tmp/wordpress.tar.gz" [1]
+ tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C /tmp/wordpress/
+ wget -nv -O /tmp/wordpress//wp-content/db.php https://raw.github.com/markoheijnen/wp-mysqli/master/db.php
2015-03-25 15:58:18 URL:https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php [87/87] -> "/tmp/wordpress//wp-content/db.php" [1]
+ install_test_suite
++ uname -s
+ [[ Linux == \D\a\r\w\i\n ]]
+ local ioption=-i
+ mkdir -p /tmp/wordpress-tests-lib
+ cd /tmp/wordpress-tests-lib
+ svn co --quiet https://develop.svn.wordpress.org/trunk/tests/phpunit/includes/
+ wget -nv -O wp-tests-config.php https://develop.svn.wordpress.org/trunk/wp-tests-config-sample.php
2015-03-25 15:58:19 URL:https://develop.svn.wordpress.org/trunk/wp-tests-config-sample.php [1374/1374] -> "wp-tests-config.php" [1]
+ sed -i 's:dirname( __FILE__ ) . '\''/src/'\'':'\''/tmp/wordpress/'\'':' wp-tests-config.php
+ sed -i s/youremptytestdbnamehere/testdb/ wp-tests-config.php
+ sed -i s/yourusernamehere/testuser/ wp-tests-config.php
+ sed -i s/yourpasswordhere/testpass/ wp-tests-config.php
+ sed -i 's|localhost|localhost|' wp-tests-config.php
+ install_db
+ PARTS=(${DB_HOST//\:/ })
+ local PARTS
+ local DB_HOSTNAME=localhost
+ local DB_SOCK_OR_PORT=
+ local EXTRA=
+ '[' -z localhost ']'
++ grep -e '^[0-9]\{1,\}$'
++ echo
+ '[' ']'
+ '[' -z ']'
+ '[' -z localhost ']'
+ EXTRA=' --host=localhost --protocol=tcp'
+ mysqladmin create testdb --user=testuser --password=testpass --host=localhost --protocol=tcp
mysqladmin: CREATE DATABASE failed; error: 'Can't create database 'testdb'; database exists'
bin/install-wp-tests.sh

Install WP Unit Testing Suite

./tests
./
bootstrap.php
test-sample.php
<?php

class SampleTest extends WP_UnitTestCase {

	function testSample() {
		// replace this with some actual testing code
		$this->assertTrue( true );
	}
}

Install WP Unit Testing Suite

tests/bootstrap.php
<?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__ ) . '/../yaml-post-types.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

require $_tests_dir . '/includes/bootstrap.php';

Running Tests

$> phpunit 
Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
Not running ajax tests. To execute these, use --group ajax.
Not running ms-files tests. To execute these, use --group ms-files.
Not running external-http tests. To execute these, use --group external-http.
PHPUnit 3.7.28 by Sebastian Bergmann.

Configuration read from /var/www/lilysinbloom/wp-content/plugins/yaml-post-types/phpunit.xml

.

Time: 835 ms, Memory: 25.25Mb

OK (1 test, 1 assertion)
phpunit
chmod -R 0755 tests/

Writing Tests

<?php

class TestYamlfyInstance extends PHPUnit_Framework_TestCase {

  public function testYamlfyInstance() {
    $object = Yamlfy::instance();
    $this->assertInstanceOf('Yamlfy',$object);
  }

}
tests/test-yamlfy-instance.php
$ phpunit
Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
PHPUnit 3.7.28 by Sebastian Bergmann.

Configuration read from phpunit.xml

..

Time: 1.92 seconds, Memory: 25.50Mb

OK (2 tests, 2 assertions)

Writing Tests

<?php
...

  public function testVersion() {
    $yamlfy = Yamlfy::instance();
    $data = get_plugin_data( __DIR__.'/../yaml-post-types.php' );
    $this->assertEquals( $yamlfy::version, $data['Version'] );
  }
}
tests/test-yamlfy-instance.php
$ phpunit
Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
PHPUnit 3.7.28 by Sebastian Bergmann.

..F

Time: 830 ms, Memory: 25.75Mb

There was 1 failure:

1) TestYamlfyInstance::testVersion
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'0.1.0-alpha'
+'0.1-alpha'

tests/test-yamlfy-instance.php:13

FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
$ phpunit
Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
PHPUnit 3.7.28 by Sebastian Bergmann.

...

Time: 1.21 seconds, Memory: 25.50Mb

OK (3 tests, 3 assertions)

Test-Driven Development

public function init() {
    try {
      $parser = new \Symfony\Component\Yaml\Parser();
      $finder = new \Symfony\Component\Finder\Finder();
      if( !$configs = get_transient('yaml-post-types-configs') ) {
        $dirs = apply_filters('yamlfy-config-dirs', array(dirname(__FILE__)));
        $finder->name('*.yaml')->files()->in($dirs);
        $configs = array();
        foreach ( $finder as $file ) {
            $configs[] = $parser->parse( $file->getContents() );
        }
        set_transient('yaml-post-types-configs', $configs);
      }
      foreach($configs as $config) {
        $post_types = $taxonomies = $meta_boxes = $index = false;
        if (array_key_exists('post_types', $config)) {
          $this->register_post_types($config['post_types']);
        }

        if (array_key_exists('taxonomies',$config)) {
          $this->register_taxonomies($config['taxonomies']);
        }

        if (array_key_exists('meta_boxes',$config)) {
          $this->load_meta_boxes($config['meta_boxes']);
        }
      }
    } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
        $this->error("Could not parse {$file->getFilename()}:{$e->getMessage()}");
    }  catch(\Exception $e) {
      $this->error($e->getMessage());
    }
  }

Test-Driven Development

...
  public function loadConfigs() {
      $parser = new \Symfony\Component\Yaml\Parser();
      $finder = new \Symfony\Component\Finder\Finder();
      if( !$configs = get_transient('yaml-post-types-configs') ) {
        $dirs = apply_filters('yamlfy-config-dirs', array(dirname(__FILE__)));
        $finder->name('*.yaml')->files()->in($dirs);
        $configs = array();
        foreach ( $finder as $file ) {
            $configs[] = $parser->parse( $file->getContents() );
        }
        set_transient('yaml-post-types-configs', $configs);
      }
      return $configs;
  }

  public function init() {
    try {
      $configs = $this->loadConfigs();
...
 
  public function testConfigs() {
    delete_transient('yaml-post-types-configs');
    add_filter('yamlfy-config-dirs', function( $dirs ) { return array( __DIR__ ); });
    $yamlfy = Yamlfy::instance();
    $configs = $yamlfy->loadConfigs();
    foreach ($configs as $config) {
      $this->assertArrayHasKey('post_types',$config);
    }
  }

}

Test-Driven Programming

...
  public function loadConfigs() {
      $parser = new \Symfony\Component\Yaml\Parser();
      $finder = new \Symfony\Component\Finder\Finder();
      if( !$configs = get_transient('yaml-post-types-configs') ) {
        $dirs = apply_filters('yamlfy-config-dirs', array(dirname(__FILE__)));
        $finder->name('*.yaml')->files()->in($dirs);
        $configs = array();
        foreach ( $finder as $file ) {
            $configs[] = $parser->parse( $file->getContents() );
        }
        set_transient('yaml-post-types-configs', $configs);
      }
      return $configs;
  }

  public function init() {
    try {
      $configs = $this->loadConfigs();
...
 
  public function testConfigs() {
    delete_transient('yaml-post-types-configs');
    add_filter('yamlfy-config-dirs', function( $dirs ) { return array( __DIR__ ); });
    $yamlfy = Yamlfy::instance();
    $configs = $yamlfy->loadConfigs();
    foreach ($configs as $config) {
      $this->assertArrayHasKey('post_types',$config);
    }
  }

}

Continuous Integration

Awesome

Awesome

Awesome

Continous Integration

Continous Integration

Continous Integration

language: php

php:
    - 5.3
    - 5.4
    - 5.5

env:
    - WP_VERSION=latest WP_MULTISITE=0
    - WP_VERSION=latest WP_MULTISITE=1
    - WP_VERSION=3.8 WP_MULTISITE=0
    - WP_VERSION=3.8 WP_MULTISITE=1

before_script:
    - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 

script: phpunit
.travis.yml

Continous Integration

Enable in travis

Continous Integration

Write a new unit test
...

  public function testConfigs() {
    delete_transient('yaml-post-types-configs');
    add_filter('yamlfy-config-dirs', function( $dirs ) { return array( __DIR__ ); });
    $yamlfy = Yamlfy::instance();
    $configs = $yamlfy->loadConfigs();
    foreach ($configs as $config) {
      $this->assertArrayHasKey('post_types',$config);
      $yamlfy->register_post_types($config['post_types']);
    }

    // make sure our post type was registered
    $post_types = get_post_types();
    $this->assertContains('books',$post_types);

  }

Continous Integration

Git commit and push
$> git add .travis tests/ phpunit.xml bin/test.sh
$> git push origin master

Continous Integration

Git commit and push
$> git add .
$> git commit -am "Adding unit tests and travis config" 
$> git push origin master

Continous Integration

Check travis

Continous Integration

Check travis

Continous Integration

Check travis

Continous Integration

Success

The END