Be Nice to Future You

Write Unit Tests With PHPUnit


John Norton

@jukebox42




Who is this guy?

My name is John Norton and I like to write code.

I work for Dyn (http://dyn.com).

Why You Should Write Unit Tests


  • Because it's a good idea
  • Allows you to be confident that your change does not break anything else
  • Helps other developers understand what your code does (and aids the onboarding process)
  • Speeds up the development process in the future
  • Helps keep you in check when writing code. It will force you to think things such as "how am I going to test this?"
  • Helps you sleep at night

The Concept of Unit Tests

  • Unit Tests are functions and classes designed to prove the code performs withing a set of guidelines.
  • Unit Testing is most effective when your code does not contain "mammoth functions" (functions that scale hundreds of lines of code).
  • Unit Testing is much more than just writing tests. It's the corner stone of well-written, maintainable code. 

Getting Started With PHPUnit

http://phpunit.de/

Installing

(the old way)



PEAR

Installing

(the new way)



PHAR or Composer

http://phpunit.de/manual/current/en/installation.html

Example

The Code
class Unicorns {
    private $count = 8;

    public function getCount() {
        return $this->count;
    }

    public function steal($number) {
        $this->count -= $number;
    }
}

Example

The Unit Test
class UnicornsTest extends PHPUnit_Framework_TestCase {
    public function test_can_steal() {
        // Setup
        $unicorns = Unicorns();

        // Action
        $unicorns->steal(5);
        
// Assert $this->assertEquals(3, $unicorns->getCount());
} }

Detour

The Assert
 assertEquals($expected, $actual [, $message = '']);
  • Expected - The value you expect to get
  • Actual - The value you actually got (hopefully the same as expected)
  • Message - An optional param to describe the assert. Useful when running the tests.

Example

Running PHPUnit
 $ phpunit tests/unicorns.php
PHPUnit 3.4.0 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 0.5Mb

OK (1 tests, 1 assertions)

Example

Changing The Class
class Unicorns() {
    private $count = 8;

    public function getCount() {
        return $this->count;
    }

    public function steal($number) {
// Make sure we can't have negative unicorns if($this->count - $number >= 0) {
$this->count -= $number;
}
} }

Example

Changing The Tests
class UnicornsTest extends PHPUnit_Framework_TestCase() {
    public function test_can_steal() {
        $unicorns = Unicorns();
        $unicorns->steal(5);
        $this->assertEquals(3, $unicorns->getCount());
    }

public function test_cant_steal() { $unicorns = Unicorns(); $unicorns->steal(1000); $this->assertEquals(8, $unicorns->getCount()); }
}

Example

Running PHPUnit
 $ phpunit tests/unicorns.php
PHPUnit 3.4.0 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 0.5Mb

OK (2 tests, 2 assertions)

Example

The "Optimizer"
class Unicorns() {
    private $count = 8;

    public function getCount() {
        return $this->count;
    }

    public function steal($number) {
$this->count -= $this->count - $number >= 0 ? $number : 0;
} }

Example

Our Unit Tests
class UnicornsTest extends PHPUnit_Framework_TestCase() {
    public function test_can_steal() {
        $unicorns = Unicorns();
        $unicorns->steal(5);
        $this->assertEquals(3, $unicorns->getCount());
    }

    public function test_cant_steal() {
        $unicorns = Unicorns();
        $unicorns->steal(1000);
        $this->assertEquals(8, $unicorns->getCount());
    }
}

Example

Running PHPUnit
 $ phpunit tests/unicorns.php
PHPUnit 3.4.0 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 0.5Mb

OK (2 tests, 2 assertions)

Example

The Bad "Optimizer"
class Unicorns() {
    private $count = 8;

    public function getCount() {
        return $this->count;
    }

    public function steal($num) {
        $this->count -= $this->count - $num >= 0 ? $num : $this->count;
    }
}

Example

Running PHPUnit (again)
 $ phpunit tests/unicorns.php
PHPUnit 3.4.0 by Sebastian Bergmann.

.F

Time: 0 seconds, Memory: 0.5Mb

There was 1 failure:

1) UnicornsTest::test_cant_steal
Failed asserting that 0 matches expected 8.

/path/to/test/unicorns.php:11

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

Auto Detect Tests

Running PHPUnit
 $ phpunit tests/
PHPUnit 3.4.0 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 0.5Mb

OK (2 tests, 2 assertions)
Gotcha
Files must be named properly *Test

A Journey Through PHPUnit

http://phpunit.de/documentation.html

setUp()

A function that runs before every test in a test class
class UnicornsTest extends PHPUnit_Framework_TestCase() {
protected function setUp() { // Create the unicorns object for each test $this->unicorns = Unicorns(); }
public function test_can_steal() { $this->unicorns->steal(5); $this->assertEquals(3, $this->unicorns->getCount()); } public function test_cant_steal() { $this->unicorns->steal(1000); $this->assertEquals(8, $this->unicorns->getCount()); } }

tearDown()

A function that runs after every test in a test class
class UnicornsTest extends PHPUnit_Framework_TestCase() {
protected function tearDown() { // Clean up the mess you've made }
public function test_can_steal() { $this->unicorns->steal(5); $this->assertEquals(3, $this->unicorns->getCount()); } public function test_cant_steal() { $this->unicorns->steal(1000); $this->assertEquals(8, $this->unicorns->getCount()); } }

A Bunch Of Asserts

  • assertEquals()
  • assertTrue()
  • assertFalse()
  • assertLessThan()
  • assertNull()
  • assertRegExp()

I Mean TONS Of Asserts

assertArrayHasKey()
assertClassHasAttribute()
assertClassHasStaticAttribute()
assertContains()
assertContainsOnly()
assertContainsOnlyInstancesOf()
assertCount()
assertEmpty()
assertEqualXMLStructure()
assertEquals()
assertFalse()
assertFileEquals()
assertFileExists()
assertGreaterThan()
assertGreaterThanOrEqual()
assertInstanceOf()
assertInternalType()
assertJsonFileEqualsJsonFile()
assertJsonStringEqualsJsonFile()
assertJsonStringEqualsJsonString()
assertLessThan()
assertLessThanOrEqual()
assertNull()
assertObjectHasAttribute()
assertRegExp()
assertStringMatchesFormat()
assertStringMatchesFormatFile()
assertSame()
assertSelectCount()
assertSelectEquals()
assertSelectRegExp()
assertStringEndsWith()
assertStringEqualsFile()
assertStringStartsWith()
assertTag()
assertThat()
assertTrue()
assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString()

assertLessThan()

class ReactorTest extends PHPUnit_Framework_TestCase() {
    public function test_not_over() {
        $reactor = Reactor();
        $reactor->setTemp(9000);
        $this->assertLessThan(100, $reactor->getTemp());
    }
}
Output
PHPUnit 4.1.0 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ReactorTest::test_not_over
Failed asserting that 9000 is less than 100.

/path/to/test/reactor.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

assertRegExp()

class RegExpTest extends PHPUnit_Framework_TestCase {
    public function testFailure() {
        $this->assertRegExp('/foo/', 'bar');
    }
}

Output

PHPUnit 4.1.0 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) RegExpTest::testFailure
Failed asserting that 'bar' matches PCRE pattern "/foo/".

/path/to/test/RegExpTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

assertStringEndsWith()

class StringEndsWithTest extends PHPUnit_Framework_TestCase
{
    public function testFailure()
    {
        $this->assertStringEndsWith('suffix', 'foo');
    }
}
Output
PHPUnit 4.1.0 by Sebastian Bergmann.

F

Time: 1 second, Memory: 5.00Mb

There was 1 failure:

1) StringEndsWithTest::testFailure
Failed asserting that 'foo' ends with "suffix".

/path/to/test/StringEndsWithTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

http://phpunit.de/manual/current/en/appendixes.assertions.html

Exceptions

Our Code
class Unicorns() {
    private $count = 8;

    public function getCount() {
        return $this->count;
    }

    public function steal($num) {
        if ($num > $this->count) {
            throw new Exception('Not enough unicorns to steal');
        }
        $this->count -= $num;
    }
}

Exceptions

class UnicornsTest extends PHPUnit_Framework_TestCase() {
    /**
     * @expectedException        Exception
     * @expectedExceptionMessage Not enough unicorns to steal
     */
    public function test_cant_steal() {
        $unicorns = Unicorns();
        $unicorns->steal(1000);
    }
}
Output
PHPUnit 3.4.0 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 0.5Mb

OK (1 tests, 1 assertions)

Annotations

Special decorators for classes and functions to give additional metadata about the test and occasionally give them some them magical powers. 

@depends

Set test dependencies
class DependsTest extends PHPUnit_Framework_TestCase {
    public function test_one() {
        $this->assertTrue(false);
    }

    /**
     * @depends test_one
     */
    public function test_two() {
        $this->assertTrue(true);
    }
}

This does NOT define the order the tests will get run. This means that when you run the test, if a dependent fails, then the output will declare any test depending on it as "skipped".

@small @medium @large

The test will fail if the test takes longer than it "should"
class TimeTest extends PHPUnit_Framework_TestCase {
    /**
     * @small
     */
    public function test_two() {
        $result do_a_bunch_of_crazy_things();
        $this->assertTrue($result);
    }
}

@requires

You can define dependencies for whether a test should run or not
/**
 * @requires extension mysqli
 */
class DatabaseTest extends PHPUnit_Framework_TestCase {
    ...
}
You can also skip tests this way:
class DatabaseTest extends PHPUnit_Framework_TestCase {
    public function setUp() {
        if(something_is_not_met()) {
$this->markTestSkipped('Don\'t run this test!');
} } }

All The Annotations

@author
@after
@afterClass
@backupGlobals
@backupStaticAttributes
@before
@beforeClass
@codeCoverageIgnore*
@covers
@coversDefaultClass
@coversNothing
@dataProvider
@depends
@expectedException
@expectedExceptionCode
@expectedExceptionMessage
@group
@large
@medium
@preserveGlobalState
@requires
@runTestsInSeparateProcesses
@runInSeparateProcess
@small
@test
@testdox
@ticket
@uses
http://phpunit.de/manual/current/en/appendixes.annotations.html

Code Coverage

Building Confidence

What Is Code Coverage?

A tool that determines how much of your code is covered by unit tests. It also shows you what is and isn't being covered.

And so much more...



Example

$ phpunit --coverage-html sometest.php

(image stolen from PHPUnit's website)

Something To Think About

Be careful and don't blindly trust your coverage analysis. Code coverage can only tell you which lines were hit when you ran your unit tests. It doesn't "know" if your tests are actually testing it.

Other Tools Available

  • Selenium - GUI Testing (Integrates with PHPUnit)
  • phpt - unit testing for php
  • SimpleTest - Alternative to PHPUnit
 
John Norton
@jukebox42

Questions?

Be Nice To Future You - Write Unit Tests With PHPUnit

By John Norton

Be Nice To Future You - Write Unit Tests With PHPUnit

Getting started with unit testing in PHP with PHPUnit

  • 6,619