Writing Cleaner, Sturdier Code With Unit Testing
Outline
- Unit testing overview
- Examining a unit test
- Testing our own code
- Unit testing in WordPress
What is unit testing?
- Units = small parts
- Meeting expectations
Unit tests in WordPress core
- Reject an
update_option()
with an illegal key - Set the correct status to future posts
- Clear the usermeta cache after a user is deleted
What is PHPUnit?
See also: QUnit
Getting to phpunit
Next: "Setting up Vagrant for Unit Testing" with Paul Bearne
Create a plugin with unit tests
$ wp scaffold plugin my-plugin
$ ls my-plugin
> bin my-plugin.php phpunit.xml readme.txt tests
Add unit test files to an existing plugin
$ wp scaffold unit-tests my-existing-plugin
Run the default test
$ cd my-plugin/
$ phpunit
Default test output

What was the test?
<?php
// tests/test-sample.php
class SampleTest extends WP_UnitTestCase {
function testSample() {
// replace this with some actual testing code
$this->assertTrue( true );
}
}
The default test
<?php
// tests/test-sample.php
function testSample() {
// replace this with some actual testing code
$this->assertTrue( true );
}
The default test (amended)
<?php
// tests/test-sample.php
function testSample() {
$foo = true;
$this->assertTrue( $foo );
}
Assertions
assertFalse()
assertEquals()
assertInternalType()
assertArrayHasKey()
assertCount()
assertEmpty()
assertFileExists()
assertGreaterThan()
assertLessThan()
assertObjectHasAttribute()
And many more: phpunit.de/manual
Multiple tests
<?php
class SampleTest extends WP_UnitTestCase {
function test_is_true() {
$foo = true;
$this->assertTrue( $foo );
}
function test_is_null() {
$foo = null;
$this->assertNull( $foo );
}
}
Multiple tests output

Failed tests
<?php
class SampleTest extends WP_UnitTestCase {
function test_is_true() { /* ... */ }
function test_is_null() { /* ... */ }
function test_names() {
$name = 'Wordpress';
$this->assertEquals( 'WordPress', $name );
}
}
Failed test output

Testing our code
<?php
// my-plugin.php
function say_hello( $name ) {
return sprintf( __( 'Hello %s!', 'my-plugin' ), $name );
}
<?php
// tests/test-sample.php
function test_say_hello() {
$expected = 'Hello, David!';
$actual = say_hello( 'David' );
$this->assertEquals( $expected, $actual );
}
Custom code output

(Fixing the mistake)
<?php
// my-plugin.php
function say_hello( $name ) {
- return sprintf( __( 'Hello %s!', 'my-plugin' ), $name );
+ return sprintf( __( 'Hello, %s!', 'my-plugin' ), $name );
}
Rock paper scissors
<?php
function rock_wins( $opponent ) {
if ( 'scissors' == strtolower( $opponent ) ) {
return true;
} else {
return false;
}
}
Rock paper scissors
<?php
class Test_RPS extends WP_UnitTestCase {
}
Rock paper scissors
<?php
class Test_RPS extends WP_UnitTestCase {
function test_against_scissors() {
$actual = rock_wins( 'scissors' );
$this->assertTrue( $actual );
}
}
Rock paper scissors
<?php
class Test_RPS extends WP_UnitTestCase {
function test_against_scissors() {
$actual = rock_wins( 'scissors' );
$this->assertTrue( $actual );
}
function test_against_paper() {
$actual = rock_wins( 'paper' );
$this->assertFalse( $actual );
}
}
Rock paper scissors
<?php
class Test_RPS extends WP_UnitTestCase {
function test_against_scissors() {
$actual = rock_wins( 'scissors' );
$this->assertTrue( $actual );
}
function test_against_paper() {
$actual = rock_wins( 'paper' );
$this->assertFalse( $actual );
}
function test_against_banana() {
$actual = rock_wins( 'banana' );
$this->assertTrue( $actual );
}
}
RPS output

Rock paper scissors (fixed)
<?php
function rock_wins( $opponent ) {
$valid = array( 'rock', 'paper', 'scissors' );
$opponent = strtolower( $opponent );
if ( ! in_array( $opponent, $valid ) ) {
return true;
}
return 'scissors' == $opponent;
}
RPS output

Maintainable code
<?php
function rock_wins( $opponent ) {
if ( ! is_legal_throw( $opponent ) ) {
return true;
}
// ...
}
function is_legal_throw( $opponent ) {
// ...
}
Separate tests
<?php
function test_is_legal_throw() {
$actual = is_legal_throw( 'scissors' );
$this->assertTrue( $actual );
$actual = is_legal_throw( 'rock' );
$this->assertTrue( $actual );
$actual = is_legal_throw( 'banana' );
$this->assertFalse( $actual );
$actual = is_legal_throw( array( 'rock', 'paper', 'scissors' ) );
$this->assertFalse( $actual );
// ...
}
Core tests
function test_bad_option_names() {
foreach ( array( '', '0', ' ', 0, false, null ) as $empty ) {
$this->assertFalse( get_option( $empty ) );
$this->assertFalse( add_option( $empty, '' ) );
$this->assertFalse( update_option( $empty, '' ) );
$this->assertFalse( delete_option( $empty ) );
}
}
function test_returns_false_if_given_an_invalid_email_address() {
$data = array(
"khaaaaaaaaaaaaaaan!",
'http://bob.example.com/',
"sif i'd give u it, spamer!1",
"com.exampleNOSPAMbob",
"bob@your mom"
);
foreach ($data as $datum) {
$this->assertFalse(is_email($datum), $datum);
}
}
WP_UnitTestCase
go_to()
function test_demonstrations() {
$link = get_post_type_archive_link( 'my-post-type' );
$this->go_to( $link );
}
assertWPError()
function test_demonstrations() {
$response = wp_update_post( array( 'ID' => 'oops' ), true );
$this->assertWPError( $response );
}
factory->post
function test_demonstrations() {
$post_ID_A = $this->factory->post->create();
$post_ID_B = $this->factory->post->create( array(
'post_title' => 'Hello, Toronto!',
'post_date' => '2014-11-16 09:30:00',
) );
}
factory->term
function test_demonstrations() {
$term_ID_A = $this->factory->term->create();
$term_ID_B = $this->factory->term->create( array(
'name' => 'Term B',
'taxonomy' => 'category',
) );
}
factory->user
function test_demonstrations() {
$user_ID_A = $this->factory->user->create();
$user_ID_B = $this->factory->user->create( array(
'role' => 'subscriber',
'display_name' => 'Aldo Leopold'
) );
}
get_echo()
function test_demonstrations() {
$haystack = get_echo( 'the_content' );
$this->assertContains( 'needle', $haystack );
}