Testing your PHP Codebase with EnhancePHP

Discussion in 'Design & Development' started by Samuel, Jan 21, 2012.

  1. Samuel

    Samuel
    Expand Collapse
    Admin

    Joined:
    Dec 20, 2011
    Messages:
    5,576
    Likes Received:
    71
    [​IMG]
    You know it; I know it. We should be testing our code more than we do. Part of the reason we don’t, I think, is that we don’t know exactly how. Well, I’m getting rid of that excuse today: I’m teaching you to test your PHP with the EnhancePHP framework.
    Meet EnhancePHP

    I’m not going to try to convince you to test your code; and we’re not going to discuss Test Driven Development, either. That’s been done before on Nettuts+. In that article, Nikko Bautista explains exactly why testing is a good thing and outlines a TDD workflow. Read that sometime, if you aren’t familiar with TDD. He also uses the SimpleTest library for his examples, so if you don’t like the look of EnhancePHP, you might try SimpleTest as an alternative.
    As I said, we’ll be using the EnhancePHP. It’s a great little PHP library—a single file—that offers a lot of testing functionality.​
    Start by heading over to their download page and grabbing the latest version of the framework.
    We’re going to be building a really simple Validation class to test. It won’t do too much: just return true if the item passes validation, or false if it doesn’t. So, set up a really simple little project:
    [​IMG]
    We’ll do this is a semi-TDD fashion, so let’s start by writing a few tests.
    Writing Tests

    Out little class is going to validate three things: email addresses, usernames, and phone numbers.
    But before we get to writing actual tests, we’ll need to set up our class:

    <?php

    class Validation_test extends \Enhance\TestFixture {
    public function setUp () {
    $this-> val = new Validation();
    }

    }

    This is our start; notice that we’re extending the class \Enhance\TestFixture. By doing so, we let EnhancePHP know that any public methods of this class are tests, with the exception of methods setUp and tearDown. As you might guess, these methods run before and after all your tests (not before and after each one). In this case, our setUp method will create a new Validation instance and assign it to a property on our instance.
    By the way, if you’re relatively new to PHP, you might not be familiar with that \Enhance\TestFixture syntax: what’s with the slashes? That’s PHP namespacing for you; check out the docs if you aren’t familiar with it.
    So, the tests!
    Email Addresses

    Let’s start by validating email addresses. As you’ll see, just doing a basic test is pretty simple:

    public function validates_a_good_email_address () {
    $result = $this->val->validate_email("john@doe.com");
    \Enhance\Assert::isTrue($result);
    }

    We simply call the method we want to test, passing it a valid email address, and storing the $result. Then, we hand $result to the isTrue method. That method belongs to the \Enhance\Assert class.
    We want to make sure our class will reject non-email addresses. So, let’s test for that:

    public function reject_bad_email_addresses () {
    $val_wrapper = \Enhance\Core::getCodeCoverageWrapper('Validation');
    $val_email = $this->get_scenario('validate_email');
    $addresses = array("john", "jo!hn@doe.com", "john@doe.", "jo*hn@doe.com");

    foreach ($addresses as $addr) {
    $val_email->with($addr)->expect(false);
    }
    $val_email->verifyExpectations();
    }

    This introduces a pretty cool feature of EnhancePHP: scenarios. We want to test a bunch of non-email addresses to make sure our method will return false. By creating a scenario, we essentially wrap an instance of our class in some EnhancePHP goodness, are write much less code to test all our non-addresses. That’s what $val_wrapper is: a modified instance of our Validation class. Then, $val_email is the scenario object, somewhat like a shortcut to the validate_email method.
    Then, we’ve got an array of strings that should not validate as email addresses. We’ll loop over that array with a foreach loop. Notice how we run the test: we call the with method on our scenario object, passing it the parameters for the method we’re testing. Then, we call the expect method on that, and pass it whatever we expect to get back.
    Finally, we call the scenario’s verifyExpectations method.
    So, the first tests are written; how do we run them?
    Running Tests

    Before we actually run the tests, we’ll need to create our Validation class. Inside lib.validation.php, start with this:

    <?php

    class Validation {
    public function validate_email ($address) {

    }
    }

    Now, in test.php, we’ll pull it all together:

    <?php

    require "vendor/EnhanceTestFramework.php";
    require "lib/validation.php";
    require "test/validation_test.php";

    \Enhance\Core::runTests();

    First, we’ll require all the necessary files. Then, we call the runTests method, which finds our tests.
    Next comes the neat part. Fire up a server, and you’ll get some nice HTML output:
    [​IMG]
    Very nice, right? Now, if you’ve got PHP in your terminal, run this is in the terminal:
    [​IMG]
    EnhancePHP notices that you’re in a different environment, and adjusts its output appropriately. A side benefit of this is that if you’re using an IDE, like PhpStorm, that can run unit tests, you can view this terminal output right inside the IDE.
    You can also get XML and TAP output, if that’s what you prefer, just pass \Enhance\TemplateType::Xml or \Enhance\TemplateType::Tap to the runTests method to get the appropriate output. Note that running it in the terminal will also produce command-line results, no matter what you pass to runTests.
    Getting the Tests to Pass

    Let’s write the method that causes our tests to pass. As you know, that’s the validate_email. At the top of the Validation class, let’s define a public property:

    public $email_regex = '/^[\w+-_\.]+@[\w\.]+\.\w+$/';

    I’m putting this in a public property so that if the user wants to replace it with their own regex, they could. I’m using this simple version of an email regex, but you can replace it with your favourite regex if you want.
    Then, there’s the method:

    public function validate_email ($address) {
    return preg_match($this->email_regex, $address) == 1
    }

    Now, we run the tests again, and:
    [​IMG]
    Writing More Tests

    Time for more tests:
    Usernames

    Let’s create some tests for usernames now. Our requirements are simply that it must be a 4 to 20 character string consisting only of word characters or periods. So:

    public function validates_a_good_username () {
    $result = $this->val->validate_username("some_user_name.12");
    \Enhance\Assert::isTrue($result);
    }

    Now, how about a few usernames that shouldn’t validate:

    public function rejects_bad_usernames () {
    $val_username = $this->get_scenario('validate_username');
    $usernames = array(
    "name with space",
    "no!exclaimation!mark",
    "ts",
    "thisUsernameIsTooLongItShouldBeBetweenFourAndTwentyCharacters");

    foreach ($usernames as $name) {
    $val_username->with($name)->expect(false);
    }
    $val_username->verifyExpectations();
    }

    This is very similar to our reject_bad_email_addresses function. Notice, however, that we’re calling this get_scenario method: where’s that come from? I’m abstracting the scenario creation functionality into private method, at the bottom of our class:

    private function get_scenario ($method) {
    $val_wrapper = \Enhance\Core::getCodeCoverageWrapper('Validation');
    return \Enhance\Core::getScenario($val_wrapper, $method);
    }

    We can use this in our reject_bad_usernames and replace the scenario creation in reject_bad_email_addresses as well. Because this is a private method, EnhancePHP won’t try to run it as a normal test, the way it will with public methods.
    We’ll make these tests pass similarly to how we made the first set pass:

    # At the top . . .
    public $username_regex = '/^[\w\.]{4,20}$/';

    # and the method . . .
    public function validate_username ($username) {
    return preg_match($this->username_regex, $username) == 1;
    }

    This is pretty basic, of course, but that’s all that’s needed to meet our goal. If we wanted to return an explanation in the case of failure, you might do something like this:

    public function validate_username ($username) {
    $len = strlen($username);
    if ($len < 4 || $len > 20) {
    return "Username must be between 4 and 20 characters";
    } elseif (preg_match($this->username_regex, $username) == 1) {
    return true;
    } else {
    return "Username must only include letters, numbers, underscores, or periods.";
    }
    }

    Of course, you might also want to check if the username already exists.
    Now, run the tests and you should see them all passing.
    Phone Numbers

    I think you’re getting the hang of this by now, so let’s finish of our validation example by checking phone numbers:

    public function validates_good_phonenumbers () {
    $val_phonenumber = $this->get_scenario("validate_phonenumber");
    $numbers = array("1234567890", "(890) 123-4567",
    "123-456-7890", "123 456 7890", "(123) 456 7890");

    foreach($numbers as $num) {
    $val_phonenumber->with($num)->expect(true);
    }
    $val_phonenumber->verifyExpectations();
    }

    public function rejects_bad_phonenumnbers () {
    $result = $this->val->validate_phonenumber("123456789012");
    \Enhance\Assert::isFalse($result);
    }

    You can probably figure out the Validation method:

    public $phonenumber_regex = '/^\d{10}$|^(\(?\d{3}\)?[ |-]\d{3}[ |-]\d{4})$/';

    public function validate_phonenumber ($number) {
    return preg_match($this->phonenumber_regex, $number) == 1;
    }

    Now, we can run all the tests together. Here’s what that looks like from the command line (my preferred testing environment):
    [​IMG]
    Other Test Functionality

    Of course, EnhancePHP can do a lot more than what we’ve looked at in this little example. Let’s look at some of that now.
    We very briefly met the \Enhance\Assert class in our first test. We didn’t really use it otherwise, because it’s not useful when using scenarios. However, it’s where all the assertion methods are. The beauty of them is that their names make their functionality incredibly obvious. The following test examples would pass:
    • \Enhance\Assert::areIdentical("Nettuts+", "Nettuts+")
    • \Enhance\Assert::areNotIdentical("Nettuts+", "Psdtuts+")
    • \Enhance\Assert::isTrue(true)
    • \Enhance\Assert::isFalse(false)
    • \Enhance\Assert::contains("Net", "Nettuts+")
    • \Enhance\Assert::isNull(null)
    • \Enhance\Assert::isNotNull('Nettust+')
    • \Enhance\Assert::isInstanceOfType('Exception', new Exception(""))
    • \Enhance\Assert::isNotInstanceOfType('String', new Exception(""))
    There are a few other assertion methods, too; you can check the docs for a complete list and examples.
    Mocks

    EnhancePHP can also do mocks and stubs. Haven’t heard of mocks and stubs? Well, they aren’t too complicated. A mock is a wrapper for object, that can keep track of what methods are called, with what properties they are called, and what values are returned. A mock will have some test to verify, as we’ll see.
    Here’s a small example of a mock. Let’s start with a very simple class that counts:

    <?php

    require "vendor/EnhanceTestFramework.php";

    class Counter {
    public $num = 0;
    public function increment ($num = 1) {
    $this->num = $this->num + $num;
    return $this->num;
    }
    }

    We have one function: increment, that accepts a parameter (but defaults to 1), and increments the $num property by that number.
    We might use this class if we were building a scoreboard:

    class Scoreboard {
    public $home = 0;
    public $away = 0;

    public function __construct ($home, $away) {
    $this->home_counter = $home;
    $this->away_counter = $away;
    }

    public function score_home () {
    $this->home = $this->home_counter->increment();
    return $this->home;
    }
    public function score_away () {
    $this->away = $this->away_counter->increment();
    return $this->home;
    }
    }

    Now, we want to test to make sure that the Counter instance method increment is working properly when the Scoreboard instance methods call it. So we creat this test:

    class ScoreboardTest extends \Enhance\TestFixture {
    public function score_home_calls_increment () {
    $home_counter_mock = \Enhance\MockFactory::createMock("Counter");
    $away_counter = new Counter();

    $home_counter_mock->addExpectation( \Enhance\Expect::method('increment') );

    $scoreboard = new Scoreboard($home_counter_mock, $away_counter);
    $scoreboard->score_home();

    $home_counter_mock->verifyExpectations();
    }
    }

    \Enhance\Core::runTests();

    Notice that we start by creating $home_counter_mock: we use the EnhancePHP mock factory, passing it the name of the class we’re mocking. This returns a “wrapped” instance of Counter. Then, we add an expectation, with this line

    $home_counter_mock->addExpectation( \Enhance\Expect::method('increment') );

    Our expectation just says that we expect the increment method to be called.
    After that, we go on to create the Scoreboard instance, and call score_home. Then, we verifyExpectations. If you run this, you’ll see that our test passes.
    We could also state what parameters we want a method on the mock object to be called with, what value is returned, or how many times the method should be called, with something like this:

    $home_counter_mock->addExpectation( \Enhance\Expect::method('increment')->with(10) );
    $home_counter_mock->addExpectation( \Enhance\Expect::method('increment')->times(2) );
    $home_counter_mock->addExpectation( \Enhance\Expect::method('increment')->returns(1) );
    $home_counter_mock->addExpectation( \Enhance\Expect::method('increment')->with(3)->times(1) );
    $home_counter_mock->addExpectation( \Enhance\Expect::method('increment')->with(2)->returns(2) );

    I should mention that, while with and times will show failed tests if the expectations aren’t meant, returns doesn’t. You’ll have to store the return value and use an assertion to very that. I’m not sure why that’s the case, but every library has its quirks :). (You can see an example of this in the library examples in Github.)
    Stubs

    Then, there are stubs. A stub fills in for a real object and method, returning exactly what you tell it to. So, let’s say we want to make sure that our Scoreboard instance is correctly using the value it receives from increment, we can stub a Counter instance so we can control what increment will return:

    class ScoreboardTest extends \Enhance\TestFixture {
    public function score_home_calls_increment () {
    $home_counter_stub = \Enhance\StubFactory::createStub("Counter");
    $away_counter = new Counter();

    $home_counter_stub->addExpectation( \Enhance\Expect::method('increment')->returns(10) );

    $scoreboard = new Scoreboard($home_counter_stub, $away_counter);
    $result = $scoreboard->score_home();

    \Enhance\Assert::areIdentical($result, 10);

    }
    }

    \Enhance\Core::runTests();

    Here, we’re using \Enhance\StubFactory::createStub to create our stub counter. Then, we add an expectation that the method increment will return 10. We can see that the result it what we’d expect, given our code.
    For more examples of mocks and stub with the EnhancePHP library, check out the Github Repo.
    Conclusion

    Well, that’s a look at testing in PHP, using the EnhancePHP framework. It’s an incredibly simple framework, but it provides everything you need to do some simple unit testing on your PHP code. Even if you choose a different method/framework for testing your PHP (or perhaps roll your own!), I hope this tutorial has sparked an interest in testing your code, and how simple it can be.
    But maybe you already test your PHP. Let us all know what you use in the comments; after all, we’re all here to learn from each other! Thank you so much for stopping by!
    [​IMG]

    [​IMG]
    [​IMG]
    [​IMG] [​IMG] [​IMG] [​IMG] [​IMG]

    Continue reading...
     

Share This Page