Tài liệu PHP Objects, Patterns and Practice- P9 - Pdf 87

C H A P T E R 18

■ ■ ■

379
Testing with PHPUnit
Every component in a system depends, for its continued smooth running, on the consistency of
operation and interface of its peers. By definition, then, development breaks systems. As you improve
your classes and packages, you must remember to amend any code that works with them. For some
changes, this can create a ripple effect, affecting components far away from the code you originally
changed. Eagle-eyed vigilance and an encyclopedic knowledge of a system’s dependencies can help to
address this problem. Of course, while these are excellent virtues, systems soon grow too complex for
every unwanted effect to be easily predicted, not least because systems often combine the work of many
developers. To address this problem, it is a good idea to test every component regularly. This, of course,
is a repetitive and complex task and as such it lends itself well to automation.
Among the test solutions available to PHP programmers, PHPUnit is perhaps the most ubiquitous
and certainly the most fully featured tool. In this chapter, you will learn the following about PHPUnit:
• Installation: Using PEAR to install PHPUnit
• Writing Tests: Creating test cases and using assertion methods
• Handling Exceptions: Strategies for confirming failure
• Running multiple tests: Collecting tests into suites
• Constructing assertion logic: Using constraints
• Faking components: Mocks and stubs
• Testing web applications: With and without additional tools.
Functional Tests and Unit Tests
Testing is essential in any project. Even if you don’t formalize the process, you must have found yourself
developing informal lists of actions that put your system through its paces. This process soon becomes
wearisome, and that can lead to a fingers-crossed attitude to your projects.
One approach to testing starts at the interface of a project, modeling the various ways in which a
user might negotiate the system. This is probably the way you would go when testing by hand, although
there are various frameworks for automating the process. These functional tests are sometimes called

function addUser( $name, $mail, $pass ) {
if ( isset( $this->users[$mail] ) ) {
throw new Exception(
"User {$mail} already in the system");
}

if ( strlen( $pass ) < 5 ) {
throw new Exception(
"Password must have 5 or more letters");
}

$this->users[$mail] = array( 'pass' => $pass,
'mail' => $mail,
'name' => $name );
return true;
}

function notifyPasswordFailure( $mail ) {
if ( isset( $this->users[$mail] ) ) {
$this->users[$mail]['failed']=time();
}
}

function getUser( $mail ) {
return ( $this->users[$mail] );
}
}
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT
381

return false;
}
}
The class requires a UserStore object, which it saves in the $store property. This property is used by
the validateUser() method to ensure first of all that the user referenced by the given e-mail address
exists in the store and secondly that the user’s password matches the provided argument. If both these
conditions are fulfilled, the method returns true. Once again, I might test this as I go along:
$store = new UserStore();
$store->addUser( "bob williams", "[email protected]", "12345" );
$validator = new Validator( $store );
if ( $validator->validateUser( "[email protected]", "12345" ) ) {
print "pass, friend!\n";
}
I instantiate a UserStore object, which I prime with data and pass to a newly instantiated Validator
object. I can then confirm a user name and password combination.
Once I’m finally satisfied with my work, I could delete these sanity checks altogether or comment
them out. This is a terrible waste of a valuable resource. These tests could form the basis of a harness to
scrutinize the system as I develop. One of the tools that might help me to do this is PHPUnit.
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT
382
Introducing PHPUnit
PHPUnit is a member of the xUnit family of testing tools. The ancestor of these is SUnit, a framework
invented by Kent Beck to test systems built with the Smalltalk language. The xUnit framework was
probably established as a popular tool, though, by the Java implementation, jUnit, and by the rise to
prominence of agile methodologies like Extreme Programming (XP) and Scrum, all of which place great
emphasis on testing.
The current incarnation of PHPUnit was created by Sebastian Bergmann, who changed its name
from PHPUnit2 (which he also authored) early in 2007 and shifted its home from the pear.php.net
channel to pear.phpunit.de. For this reason, you must tell the pear application where to search for the

those that house the system’s classes. With a logical structure like this, you can often open up a test from
the command line without even looking to see if it exists! Each test in a test case class is run in isolation
from its siblings. The setUp() method is automatically invoked for each test method, allowing us to set
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT
383
up a stable and suitably primed environment for the test. tearDown() is invoked after each test method is
run. If your tests change the wider environment of your system, you can use this method to reset state.
The common platform managed by setUp() and tearDown() is known as a fixture.
In order to test the UserStore class, I need an instance of it. I can instantiate this in setUp() and
assign it to a property. Let’s create a test method as well:
require_once('UserStore.php');
require_once('PHPUnit/Framework/TestCase.php'); class UserStoreTest extends PHPUnit_Framework_TestCase {
private $store;

public function setUp() {
$this->store = new UserStore();
}

public function tearDown() {
}

public function testGetUser() {
$this->store->addUser( "bob williams", "[email protected]", "12345" );
$user = $this->store->getUser( "[email protected]" );
$this->assertEquals( $user['mail'], "[email protected]" );
$this->assertEquals( $user['name'], "bob williams" );

PHPUnit_Framework_TestCase Assert Methods
Method Description
assertEquals( $val1, $val2, $delta, $message)
Fail if
$val1
is not equivalent to
$val2
. (
$delta

represents an allowable margin of error.)
assertFalse( $expression, $message) Evaluate $expression. Fail if it does not
resolve to false.
assertTrue( $expression, $message) Evaluate $expression. Fail if it does not
resolve to true.
assertNotNull( $val, $message ) Fail if $val is null.
assertNull( $val, $message ) Fail if $val is anything other than null.
assertSame( $val1, $val2, $message ) Fail if $val1 and $val2 are not references to
the same object or if they are variables of
different types or values.
assertNotSame( $val1, $val2, $message ) Fail if $val1 and $val2 are references to the
same object or variables of the same type and
value.
assertRegExp( $regexp, $val, $message ) Fail if $val is not matched by regular
expression $regexp.
assertType( $typestring, $val, $message ) Fail if $val is not the type described in $type.
assertAttributeSame($val, $attribute,
$classname, $message)
Fail if $val is not the same type and value as
$classname::$attribute.

test will fail.
Here’s a quick reimplementation of the previous test:
require_once('PHPUnit/Framework/TestCase.php');
require_once('UserStore.php');

class UserStoreTest extends PHPUnit_Framework_TestCase {
private $store;

public function setUp() {
$this->store = new UserStore();
}

public function testAddUser_ShortPass() {
$this->setExpectedException('Exception');
$this->store->addUser( "bob williams", "[email protected]", "ff" );
}
}
Running Test Suites
If I am testing the UserStore class, I should also test Validator. Here is a cut-down version of a class
called ValidateTest that tests the Validator::validateUser() method:
require_once('UserStore.php');
require_once('Validator.php');
require_once('PHPUnit/Framework/TestCase.php'); class ValidatorTest extends PHPUnit_Framework_TestCase {
private $validator;

Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT

Constraints
In most circumstances, you will use off-the-peg assertions in your tests. In fact, at a stretch you can
achieve an awful lot with AssertTrue() alone. As of PHPUnit 3.0, however, PHPUnit_Framework_TestCase
includes a set of factory methods that return PHPUnit_Framework_Constraint objects. You can combine
these and pass them to PHPUnit_Framework_TestCase::AssertThat() in order to construct your own
assertions.
It’s time for a quick example. The UserStore object should not allow duplicate e-mail addresses to
be added. Here’s a test that confirms this:
class UserStoreTest extends PHPUnit_Framework_TestCase {
//....

public function testAddUser_duplicate() {
try {
$ret = $this->store->addUser( "bob williams", "[email protected]", "123456" );
$ret = $this->store->addUser( "bob stevens", "[email protected]", "123456" );
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT
387
self::fail( "Exception should have been thrown" );
} catch ( Exception $e ) {
$const = $this->logicalAnd(
$this->logicalNot( $this->contains("bob stevens")),
$this->isType('array')
);
self::AssertThat( $this->store->getUser( "[email protected]"), $const );
}
}
This test adds a user to the UserStore object and then adds a second user with the same e-mail
address. The test thereby confirms that an exception is thrown with the second call to addUser(). In the
catch clause, I build a constraint object using the convenience methods available to us. These return

greaterThan( $num )
Test value is greater than
$num
.
contains( $val )
Test value (traversable) contains an
element that matches
$val
.
identicalTo( $val )
Test value is a reference to the same object
as
$val
or, for non-objects, is of the same
type and value.
greaterThanOrEqual( $num )

Test value is greater than or equal to
$num
.
lessThan( $num )

Test value is less than
$num
.
lessThanOrEqual( $num )

Test value is less than or equal to
$num
.

[, $const..])

At least one of the provided constraints
match.
logicalNot( PHPUnit_Framework_Constraint $const )

The provided constraint does not pass.
Mocks and Stubs
Unit tests aim to test a component in isolation of the system that contains it to the greatest possible
extent. Few components exist in a vacuum, however. Even nicely decoupled classes require access to
other objects as methods arguments. Many classes also work directly with databases or the filesystem.
You have already seen one way of dealing with this. The setUp() and tearDown() methods can be
used to manage a fixture, that is, a common set of resources for your tests, which might include database
connections, configured objects, a scratch area on the file system, and so on.
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT
389
Another approach is to fake the context of the class you are testing. This involves creating objects
that pretend to be the objects that do real stuff. For example, you might pass a fake database mapper to
your test object’s constructor. Because this fake object shares a type with the real mapper class (extends
from a common abstract base or even overrides the genuine class itself), your subject is none the wiser.
You can prime the fake object with valid data. Objects that provide a sandbox of this sort for unit tests
are known as stubs. They can be useful because they allow you to focus in on the class you want to test
without inadvertently testing the entire edifice of your system at the same time.
Fake objects can be taken a stage further than this, however. Since the object you are testing is likely
to call a fake object in some way, you can prime it to confirm the invocations you are expecting. Using a
fake object as a spy in this way is known as behavior verification, and it is what distinguishes a mock
object from a stub.
You can build mocks yourself by creating classes hard-coded to return certain values and to report
on method invocations. This is a simple process, but it can be time consuming.

which can then be invoked with a further modifying method call (itself returning an object). This can
make for easy use but painful debugging.
In the previous example, I called the PHPUnit_Framework_TestCase method: getMock(), passing it
"UserStore", the name of the class I wish to mock. This dynamically generates a class and instantiates an
object from it. I store this mock object in $store and pass it to Validator. This causes no error, because
the object’s newly minted class extends UserStore. I have fooled Validator into accepting a spy into its
midst.
Mock objects generated by PHPUnit have an expects() method. This method requires a matcher
object (actually it’s of type PHPUnit_Framework_MockObject_Matcher_Invocation, but don’t worry; you can
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT
390
use the convenience methods in TestCase to generate your matcher). The matcher defines the
cardinality of the expectation, that is, the number of times a method should be called.
Table 18–3 shows the matcher methods available in a TestCase class.
Table 18–3.

Some Matcher Methods
TestCase Method Match Fails Unless . . .
any( )

Zero or more calls are made to corresponding method (useful for stub
objects that return values but don’t test invocations).
never( )

No calls are made to corresponding method.
atLeastOnce( )

One or more calls are made to corresponding method.
once( )

Notice that I passed a constraint to with(). Actually, that’s redundant—any bare arguments are
converted to constraints internally, so I could write the statement like this:
$store->expects($this->once() )
->method('notifyPasswordFailure')
->with( '[email protected]' );
Sometimes, you only want to use PHPUnit’s mocks as stubs, that is, as objects that return values to
allow your tests to run. In such cases you can invoke InvocationMocker::will() from the call to
method(). The will() method requires the return value (or values if the method is to be called
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT
391
repeatedly) that the associated method should be primed to return. You can pass in this return value by
calling either TestCase::returnValue() or TestCase::onConsecutiveCalls(). Once again, this is much
easier to do than to describe. Here’s the fragment from my earlier example in which I prime UserStore to
return a value:
$store->expects( $this->any() )
->method("getUser")
->will( $this->returnValue(
array( "name"=>"bob williams",
"mail"=>"[email protected]",
"pass"=>"right")));
I prime the UserStore mock to expect any number of calls to getUser()— right now, I’m concerned
with providing data and not with testing calls. Next, I call will() with the result of invoking
TestCase::returnValue() with the data I want returned (this happens to be a
PHPUnit_Framework_MockObject_Stub_Return object, though if I were you, I’d just remember the
convenience method you use to get it).
You can alternatively pass the result of a call to TestCase::onConsecutiveCalls() to will(). This
accepts any number of parameters, each one of which will be returned by your mocked method as it is
called repeatedly.
Tests Succeed When They Fail

CHAPTER 18 ■ TESTING WITH PHPUNIT
392
}
return null;
}
}
Here is the simple User class:
class User {
private $name;
private $mail;
private $pass;
private $failed;

function __construct( $name, $mail, $pass ) {

if ( strlen( $pass ) < 5 ) {
throw new Exception(
"Password must have 5 or more letters");
}

$this->name = $name;
$this->mail = $mail;
$this->pass = $pass;
}

function getName() {
return $this->name;
}

function getMail() {

PHPUnit 3.0.6 by Sebastian Bergmann.

...FF

Time: 00:00

There were 2 failures:

1) testValidate_CorrectPass(ValidatorTest)
Expecting successful validation
Failed asserting that <boolean:false> is identical to <boolean:true>.
/project/wibble/ValidatorTest.php:22

2) testValidate_FalsePass(ValidatorTest)
Expectation failed for method name is equal to <string:notifyPasswordFailure> ➥
when invoked 1 time(s).
Expected invocation count is wrong.

FAILURES!
Tests: 5, Failures: 2.
There is a problem with ValidatorTest. Let’s take another look at the Validator::validateUser()
method:
public function validateUser( $mail, $pass ) {

if ( ! is_array($user = $this->store->getUser( $mail )) ) {
return false;
}
if ( $user['pass'] == $pass ) {
return true;
}

an instance from the command line or method argument lists as from request parameters. The system
can then run in ignorance of its context.
If you find a system hard to run in different contexts, that may indicate a design issue. If, for
example, you have numerous filepaths hardcoded into components, it’s likely you are suffering from
tight coupling. You should consider moving elements that tie your components to their context into
encapsulating objects that can be acquired from a central repository. The registry pattern, also covered
in Chapter 12, will likely help you with this.
Once your system can be run directly from a method call, you’ll find that high level web tests are
relatively easy to write without any additional tools.
You may find, however, that even the most well thought-out project will need some refactoring to
get things ready for testing. In my experience, this almost always results in design improvements. I’m
going to demonstrate this by retrofitting one aspect the WOO example from Chapters 12 and 13 for unit
testing.
Refactoring a Web Application for Testing
We actually left the WOO example in a reasonable state from a tester’s point of view. Because the
system uses a single Front Controller, there’s a simple API interface. This is a simple class called
Runner.php.
require_once( "woo/controller/Controller.php");
\woo\controller\Controller::run();
That would be easy enough to add to a unit test, right? But what about command line arguments?
To some extent, this is already handled in the Request class:
// \woo\controller\Request
function init() {
if ( isset( $_SERVER['REQUEST_METHOD'] ) ) {
$this->properties = $_REQUEST;
return;
}

foreach( $_SERVER['argv'] as $arg ) {
if ( strpos( $arg, '=' ) ) {

<input type="submit" value="submit" />
</form>
</body>
</html>
Although this works for the command line, it remains a little tricky to pass in arguments via a
method call. One inelegant solution would be to manually set the $argv array before calling the
controller’s run() method. I don’t much like this, though. Playing directly with magic arrays feels plain
wrong, and the string manipulation involved at each end would compound the sin. Looking at the
controller class more closely, I see an opportunity to improve both design and testability. Here’s an
extract from the handleRequest() method:
// \woo\controller\Controller
function handleRequest() {
$request = new Request();
$app_c = \woo\base\ApplicationRegistry::appController();
while( $cmd = $app_c->getCommand( $request ) ) {
$cmd->execute( $request );
}
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT
396
\woo\domain\ObjectWatcher::instance()->performOperations();
$this->invokeView( $app_c->getView( $request ) );
}
This method is designed to be invoked by the static run() method. The first thing I notice is a very
definite code smell. The Request object is directly instantiated here. That means I can’t swap in a stub
should I want to. Time to pull on the thread. What’s going on in Request? This is the constructor:
// \woo\controller\Request
function __construct() {
$this->init();
\woo\base\RequestRegistry::setRequest($this );

function handleRequest() {

$request = \woo\base\RequestRegistry::getRequest();
$app_c = \woo\base\ApplicationRegistry::appController();
while( $cmd = $app_c->getCommand( $request ) ) {
$cmd->execute( $request );
}
\woo\domain\ObjectWatcher::instance()->performOperations();
$this->invokeView( $app_c->getView( $request ) );
}
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
CHAPTER 18 ■ TESTING WITH PHPUNIT
397
With those refactorings out the way, my system is more amenable to testing. It’s no accident that my
design has improved at the same time. Now it’s to begin writing tests.
Simple Web Testing
Here’s a test case that performs a very basic test on the WOO system:
class AddVenueTest extends PHPUnit_Framework_TestCase {

function testAddVenueVanilla() {
$this->runCommand("AddVenue", array("venue_name"=>"bob") );
}

function runCommand( $command=null, array $args=null ) {
$request = \woo\base\RequestRegistry::getRequest();
if ( ! is_null( $args ) ) {
foreach( $args as $key=>$val ) {
$request->setProperty( $key, $val );
}
}

CHAPTER 18 ■ TESTING WITH PHPUNIT
398
woo\controller\Controller::run();
$ret = ob_get_contents();
ob_end_clean();
return $ret;
}
}
By catching the system's output in a buffer, I’m able to return it from the runCommand() method. I
apply a simple assertion to the return value to demonstrate.
Here is the view from the command line:
$ phpunit test/AddVenueTest.php
PHPUnit 3.4.11 by Sebastian Bergmann.
.
Time: 0 seconds, Memory: 4.00Mb
OK (1 test, 1 assertion)

If you are going to be running lots of tests on a system in this way, it would make sense to create a
Web UI superclass to hold runCommand().
I am glossing over some details here that you will face in your own tests. You will need to ensure that
the system works with configurable storage locations. You don’t want your tests going to the same
datastore that you use for your development environment. This is another opportunity for design
improvement. Look for hardcoded filepaths, and DSN values, push them back to the Registry, and then
ensure your tests work within a sandbox, but setting these values in your test case’s setUp() method.
Look into swapping in a MockRequestRegistry, which you can charge up with stubs, mocks, and various
other sneaky fakes.
Approaches like this are great for testing the inputs and output of a web application. There are some
distinct limitations, however. This method won’t capture the browser experience. Where a web
application uses JavaScript, Ajax, and other client-side cleverness, testing the text generated by your
system, won't tell you whether the user is seeing a sane interface.


Nhờ tải bản gốc
Music ♫

Copyright: Tài liệu đại học © DMCA.com Protection Status