Writing Cleaner, Sturdier Code With Unit Testing

Outline

  1. Unit testing overview
  2. Examining a unit test
  3. Testing our own code
  4. 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

Output of phpunit after the default test

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

Output of phpunit after running multiple tests

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

Output of phpunit after running a failed test

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

Output of phpunit after running a failed test with custom code

(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

Output of phpunit after a failed rock-paper-scissors test

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

Output of phpunit after successful rock-paper-scissors tests

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 );
}

More resources

Thank you