1. Trang chủ
  2. » Công Nghệ Thông Tin

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

50 509 0
Tài liệu đã được kiểm tra trùng lặp

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Tiêu đề Testing with PHPUnit
Trường học University of Information Technology
Chuyên ngành Computer Science
Thể loại Tài liệu
Định dạng
Số trang 50
Dung lượng 1,3 MB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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 E

Trang 1

■ ■ ■

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 acceptance tests, because a list of actions performed successfully can be used as criteria for signing off a project phase Using this approach, you typically treat the system as a black box—your tests remaining willfully ignorant of the hidden components that collaborate to form the system under test

Whereas functional tests operate from without, unit tests (the subject of this chapter) work from the inside out Unit testing tends to focus on classes, with test methods grouped together in test cases Each test case puts one class through a rigorous workout, checking that each method performs as advertised

Trang 2

and fails as it should The objective, as far as possible, is to test each component in isolation from its wider context This often supplies you with a sobering verdict on the success of your mission to decouple the parts of your system

Tests can be run as part of the build process, directly from the command line, or even via a web page In this chapter, I’ll concentrate on the command line

Unit testing is a good way of ensuring the quality of design in a system Tests reveal the responsibilities of classes and functions Some programmers even advocate a test-first approach You should, they say, write the tests before you even begin work on a class This lays down a class’s purpose, ensuring a clean interface and short, focused methods Personally, I have never aspired to this level of purity—it just doesn’t suit my style of coding Nevertheless, I attempt to write tests as I go Maintaining a test harness provides me with the security I need to refactor my code I can pull down and replace entire packages with the knowledge that I have a good chance of catching unexpected errors elsewhere in the system

Testing by Hand

In the last section, I said that testing was essential in every project I could have said instead that testing

is inevitable in every project We all test The tragedy is that we often throw away this good work

So, let’s create some classes to test Here is a class that stores and retrieves user information For the sake of demonstration, it generates arrays, rather than the User objects you'd normally expect to use: class UserStore {

private $users = array();

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

} }

Trang 3

This class accepts user data with the addUser() method and retrieves it via getUser() The user’s e-mail address is used as the key for retrieval If you’re like me, you’ll write some sample implementation

as you develop, just to check that things are behaving as you designed them—something like this:

$store=new UserStore();

$store->addUser( "bob williams", "bob@example.com", "12345" );

$user = $store->getUser( "bob@example.com" );

print_r( $user );

This is the sort of thing I might add to the foot of a file as I work on the class it contains The test validation is performed manually, of course; it’s up to me to eyeball the results and confirm that the data returned by UserStore::getUser() corresponds with the information I added initially It’s a test of sorts, nevertheless

Here is a client class that uses UserStore to confirm that a user has provided the correct authentication information:

class Validator { private $store;

public function construct( UserStore $store ) { $this->store = $store;

}

public function validateUser( $mail, $pass ) {

if ( ! is_array($user = $this->store->getUser( $mail )) ) { return false;

}

if ( $user['pass'] == $pass ) { return true;

} $this->store->notifyPasswordFailure( $mail );

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", "bob@example.com", "12345" );

$validator = new Validator( $store );

if ( $validator->validateUser( "bob@example.com", "12345" ) ) { print "pass, friend!\n";

Trang 4

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 framework when you install:

$ pear channel-discover pear.phpunit.de

$ pear channel-discover pear.symfony-project.com

$ pear install phpunit

Note I show commands that are input at the command line in bold to distinguish them from any output they may produce

Notice I added another channel, pear.symfony-project.com This may be needed to satisfy a dependency of PHPUnit that is hosted there

Creating a Test Case

Armed with PHPUnit, I can write tests for the UserStore class Tests for each target component should be collected in a single class that extends PHPUnit_Framework_TestCase, one of the classes made available by the PHPUnit package Here’s how to create a minimal test case class:

require_once 'PHPUnit/Framework/TestCase.php';

class UserStoreTest extends PHPUnit_Framework_TestCase {

public function setUp() { }

public function tearDown() { }

//

}

I named the test case class UserStoreTest You are not obliged to use the name of the class you are testing in the test’s name, though that is what many developers do Naming conventions of this kind can greatly improve the accessibility of a test harness, especially as the number of components and tests in the system begins to increase It is also common to group tests in package directories that directly mirror 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

Trang 5

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:

$user = $this->store->getUser( "a@b.com" );

$this->assertEquals( $user['mail'], "a@b.com" );

$this->assertEquals( $user['name'], "bob williams" );

$this->assertEquals( $user['pass'], "12345" );

} } Test methods should be named to begin with the word “test” and should require no arguments This

is because the test case class is manipulated using reflection

Note Reflection is covered in detail in Chapter 5

The object that runs the tests looks at all the methods in the class and invokes only those that match this pattern (that is, methods that begin with “test”)

In the example, I tested the retrieval of user information I don’t need to instantiate UserStore for each test, because I handled that in setUp() Because setUp() is invoked for each test, the $store property is guaranteed to contain a newly instantiated object

Within the testGetUser() method, I first provide UserStore::addUser() with dummy data, then I retrieve that data and test each of its elements

Assertion Methods

An assertion in programming is a statement or method that allows you to check your assumptions about

an aspect of your system In using an assertion you typically define an expectation that something is the case, that $cheese is "blue" or $pie is "apple" If your expectation is confounded, a warning of some kind will be generated Assertions are such a good way of adding safety to a system that some programming

Trang 6

languages support them natively and inline and allow you to turn them off in a production context (Java

is an example) PHPUnit supports assertions though a set of static methods

In the previous example, I used an inherited static method: assertEquals() This compares its two provided arguments and checks them for equivalence If they do not match, the test method will be chalked up as a failed test Having subclassed PHPUnit_Framework_TestCase, I have access to a set of assertion methods Some of these methods are listed in Table 18–1

Table 18–1. 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

Your focus as a coder is usually to make stuff work and work well Often, that mentality carries through

to testing, especially if you are testing your own code The temptation is test that a method behaves as advertised It’s easy to forget how important it is to test for failure How good is a method’s error checking? Does it throw an exception when it should? Does it throw the right exception? Does it clean up

Trang 7

after an error if for example an operation is half complete before the problem occurs? It is your role as a tester to check all of this Luckily, PHPUnit can help

Here is a test that checks the behavior of the UserStore class when an operation fails:

//

public function testAddUser_ShortPass() { try {

$this->store->addUser( "bob williams", "bob@example.com", "ff" );

} catch ( Exception $e ) { return; } $this->fail("Short password exception expected");

} //

If you look back at the UserStore::addUser() method, you will see that I throw an exception if the user’s password is less than five characters long My test attempts to confirm this I add a user with an illegal password in a try clause If the expected exception is thrown, then all is well, and I return silently The final line of the method should never be reached, so I invoke the fail() method there If the addUser() method does not throw an exception as expected, the catch clause is not invoked, and the fail() method is called

Another way to test that an exception is thrown is to use an assertion method called setExpectedException(), which requires the name of the exception type you expect to be thrown (either Exception or a subclass) If the test method exits without the correct exception having been thrown, the test will fail

Here’s a quick reimplementation of the previous test:

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:

Trang 8

public function setUp() { $store = new UserStore();

$store->addUser( "bob williams", "bob@example.com", "12345" );

$this->validator = new Validator( $store );

);

} }

So now that I have more than one test case, how do I go about running them together? The best way

is to place your test classes in a directory called test You can then specify this directory and PHPUnit will run all the tests beneath it

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", "a@b.com", "123456" );

$ret = $this->store->addUser( "bob stevens", "a@b.com", "123456" );

Trang 9

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( "a@b.com"), $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 corresponding instances of PHPUnit_Framework_Constraint Let’s break down the composite constraint

in the previous example:

$this->contains("bob stevens") This returns a PHPUnit_Framework_Constraint_TraversableContains object When passed to AssertThat, this object will generate an error if the test subject does not contain an element matching the given value ("bob stevens") I negate this, though, by passing this constraint to another:

PHPUnit_Framework_Constraint_Not Once again, I use a convenience method, available though the TestCase class (actually through a superclass, Assert)

$this->logicalNot( $this->contains("bob stevens")) Now, the AssertThat assertion will fail if the test value (which must be traversable) contains an element that matches the string "bob stevens" In this way, you can build up quite complex logical structures By the time I have finished, my constraint can be summarized as follows: “Do not fail if the test value is an array and does not contain the string "bob stevens".” You could build much more involved constraints in this way The constraint is run against a value by passing both to AssertThat() You could achieve all this with standard assertion methods, of course, but constraints have a couple

of virtues First, they form nice logical blocks with clear relationships among components (although good use of formatting may be necessary to support clarity) Second, and more importantly, a constraint

is reusable You can set up a library of complex constraints and use them in different tests You can even combine complex constraints with one another:

$const = $this->logicalAnd(

$a_complex_constraint, $another_complex_constraint );

Table 18–2 shows the some of the constraint methods available in a TestCase class

Trang 10

Table 18–2. Some Constraint Methods

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

equalTo($value, $delta=0, $depth=10) Test value equals $val If specified, $delta

defines a margin of error for numeric comparisons, and $depth determines how recursive a comparison should be for arrays

All provided constraints pass

logicalOr( PHPUnit_Framework_Constraint $const, [, $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

Trang 11

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

PHPUnit provides access to an easier and more dynamic solution It will generate mock objects on the fly for you It does this by examining the class you wish to mock and building a child class that overrides its methods Once you have this mock instance, you can call methods on it to prime it with data and to set the conditions for success

Let’s build an example The UserStore class contains a method called notifyPasswordFailure(), which sets a field for a given user This should be called by Validator when an attempt to set a password fails Here, I mock up the UserStore class so that it both provides data to the Validator object and confirms that its notifyPasswordFailure() method was called as expected:

class ValidatorTest extends PHPUnit_Framework_TestCase { //

public function testValidate_FalsePass() { $store = $this->getMock("UserStore");

$this->validator = new Validator( $store );

$store->expects($this->once() ) ->method('notifyPasswordFailure') ->with( $this->equalTo('bob@example.com') );

$store->expects( $this->any() ) ->method("getUser") ->will( $this->returnValue(array("name"=>"bob@example.com", "pass"=>"right")));

$this->validator->validateUser("bob@example.com", "wrong");

} }

Mock objects use a fluent interface, that is, a language-like structure These are much easier to use

than to describe Such constructs work from left to right, each invocation returning an object reference, 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

Trang 12

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

objects that return values but don’t test invocations)

exactly( $num ) $num calls are made to corresponding method

at( $num ) A call to corresponding method made at $num index (each method call

to a mock is recorded and indexed)

Having set up the match requirement, I need to specify a method to which it applies For instance, expects() returns an object (PHPUnit_Framework_MockObject_Builder_InvocationMocker, if you must know) that has a method called method() I can simply call that with a method name This is enough to get some real mocking done:

$store = $this->getMock("UserStore");

$store->expects( $this->once() ) ->method('notifyPasswordFailure');

I need to go further, though, and check the parameters that are passed to notifyPasswordFailure() The InvocationMocker::method() returns an instance of the object it was called on InvocationMocker includes a method name with(), which accepts a variable list of parameters to match It also accepts constraint objects, so you can test ranges and so on Armed with this, you can complete the statement and ensure the expected parameter is passed to notifyPasswordFailure()

$store->expects($this->once() ) ->method('notifyPasswordFailure') ->with( $this->equalTo('bob@example.com') );

You can see why this is known as a fluent interface It reads a bit like a sentence: “The $store object

expects a single call to the notifyPasswordFailure() method with parameter bob@example.com.”

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( 'bob@example.com' );

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

Trang 13

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"=>"bob@example.com", "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

While most agree that testing is a fine thing, you grow to really love it generally only after it has saved your bacon a few times Let’s simulate a situation where a change in one part of a system has an unexpected effect elsewhere

The UserStore class has been running for a while when, during a code review, it is agreed that it would be neater for the class to generate User objects rather than associative arrays Here is the new version:

class UserStore { private $users = array();

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

if ( isset( $this->users[$mail] ) ) { throw new Exception(

"User {$mail} already in the system");

} }

function getUser( $mail ) {

if ( isset( $this->users[$mail] ) ) { return ( $this->users[$mail] );

Trang 14

} return null;

} } Here is the simple User class:

class User { private $name;

"Password must have 5 or more letters");

}

function getMail() { return $this->mail;

}

function getPass() { return $this->pass;

}

function failed( $time ) { $this->failed = $time;

} }

Of course, I amend the UserStoreTest class to account for these changes So code designed to work with an array like this:

public function testGetUser() { $this->store->addUser( "bob williams", "a@b.com", "12345" );

$user = $this->store->getUser( "a@b.com" );

$this->assertEquals( $user['mail'], "a@b.com" );

//

is converted into code designed to work with an object like this:

public function testGetUser() { $this->store->addUser( "bob williams", "a@b.com", "12345" );

$user = $this->store->getUser( "a@b.com" );

Trang 15

$this->assertEquals( $user->getMail(), "a@b.com" );

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

} $this->store->notifyPasswordFailure( $mail );

return false;

}

I invoke getUser() Although getUser() now returns an object and not an array, my method does not generate a warning getUser() originally returned the requested user array on success or null on failure, so I validated users by checking for an array using the is_array() function Now, of course, getUser() returns an object, and the validateUser() method will always return false Without the test framework, the Validator would have simply rejected all users as invalid without fuss or warning

Now, imagine making this neat little change on a Friday night without a test framework in place

Think about the frantic text messages that would drag you out of your pub, armchair, or restaurant,

“What have you done? All our customers are locked out!”

Trang 16

The most insidious bugs don’t cause the interpreter to report that something is wrong They hide in perfectly legal code, and they silently break the logic of your system Many bugs don’t manifest where you are working; they are caused there, but the effects pop up elsewhere, days or even weeks later A test framework can help you catch at least some of these, preventing rather than discovering problems in your systems

Write tests as you code, and run them often If someone reports a bug, first add a test to your framework to confirm it; then fix the bug so that the test is passed—bugs have a funny habit of recurring

in the same area Writing tests to prove bugs and then to guard the fix against subsequent problems is

known as regression testing Incidentally, if you keep a separate directory of regression tests, remember

to name your files descriptively On one project, our team decided to name our regression tests after Bugzilla bug numbers We ended up with a directory containing 400 test files, each with a name like test_973892.php Finding an individual test became a tedious chore!

Writing Web Tests

You should engineer your web systems in such a way that they can be invoked easily from the command line or an API call In Chapter 12, you saw some tricks that might help you with this In particular, if you create a Request class to encapsulate an HTTP request, you can just as easily populate

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, '=' ) ) {

Trang 17

list( $key, $val )=explode( "=", $arg );

$this->setProperty( $key, $val );

} } }

The init() method detects whether it is running in a server context, and populates the $properties array accordingly (either directly or via setProperty()) This works fine for command line invocation It means I can run something like:

$ php runner.php cmd=AddVenue venue_name=bob

and get this response:

<input type="text" value="" name="space_name"/>

<input type="hidden" name="cmd" value="AddSpace" />

<input type="hidden" name="venue_id" value="5" />

<input type="submit" value="submit" />

// \woo\controller\Controller function handleRequest() {

$request = new Request();

$app_c = \woo\base\ApplicationRegistry::appController();

while( $cmd = $app_c->getCommand( $request ) ) { $cmd->execute( $request );

}

Trang 18

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

} That smell’s getting worse The Request class refers itself to the RequestRegistry so that other components can get it There are two things I don’t like about this on reflection First, the code implies a direct invocation must take place before the Registry is used to access the Request object And second, there’s a bit of unnecessary coupling going on The Request class doesn’t really need to know about the RequestRegistry

So how can I improve my design and make the system more amenable to testing at the same time? I prefer to push instantiations back to the RequestRegistry where possible That way later I can extend the implementation of RequestRegistry::instance() to return a MockRequestRegistry populated with fake components if I want to I love to fool my systems So first off I remove that setRequest() line from the Request object Now I push my Request instantiation back to the RequestRegistry object:

} return $that->request;

} } Finally, I must replace that direct instantiation in the Controller:

// \woo\controller\Controller function handleRequest() {

$this->invokeView( $app_c->getView( $request ) );

}

Trang 19

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

} }

if ( ! is_null( $command ) ) { $request->setProperty( 'cmd', $command );

} woo\controller\Controller::run();

} }

In fact, it does not so much test anything as prove that the system can be invoked The real work is done in the runCommand() method There is nothing terribly clever here I get a Request object from the RequestRegistry, and I populate it with the keys and values provided in the method call Because the Controller will go to the same source for its Request object, I know that it will work the values I have set Running this test confirms that all is well I see the output I expect The problem is that this output is printed by the view, and is therefore hard to test I can fix that quite easily by buffering the output:

class AddVenueTest extends PHPUnit_Framework_TestCase { function testAddVenueVanilla() {

$output = $this->runCommand("AddVenue", array("venue_name"=>"bob") );

self::AssertRegexp( "/added/", $output );

} }

if ( ! is_null( $command ) ) { $request->setProperty( 'cmd', $command );

}

Trang 20

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

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

Luckily, there is a solution

Introducing Selenium

Selenium (http://seleniumhq.org/) consists of a set of commands (sometimes called selenese) for defining web tests It also provides tools for authoring and running browser tests, as well as for binding tests to existing test platforms Luckily for us, one of these platforms is PHPUnit

In this brief introduction, I’ll author a quick WOO test using the Selenium IDE Then I’ll export the results, and run it as a PHPUnit test case

Trang 21

Selenium RC requires a more manual approach Once you’ve downloaded the package, you should find an archive called selenium-remote-control-1.0.3.zip (though, of course, your version number will probably be different) You should unzip this archive and look for a jar (Java ARchive) file somewhere like selenium-server-1.0.3/selenium-server.jar Copy this file somewhere central To proceed further, you’ll need need Java installed on your system Once you’ve confirmed this, you can start the Selenium Server

Here, I copy the server to a directory named lib in my home directory Then I start the server:

$ cp selenium-server-1.0.3/selenium-server.jar ~/lib/

$ java -jar ~/lib/selenium-server.jar

13:03:28.713 INFO - Java: Sun Microsystems Inc 14.0-b16 13:03:28.745 INFO - OS: Linux 2.6.31.5-127.fc12.i686 i386 13:03:28.787 INFO - v2.0 [a2], with Core v2.0 [a2]

13:03:29.273 INFO - RemoteWebDriver instances should connect to:

http://192.168.1.65:4444/wd/hub 13:03:29.276 INFO - Version Jetty/5.1.x 13:03:29.284 INFO - Started HttpContext[/selenium-server/driver,/selenium-server/driver]

13:03:29.286 INFO - Started HttpContext[/selenium-server,/selenium-server]

13:03:29.286 INFO - Started HttpContext[/,/]

13:03:29.383 INFO - Started org.openqa.jetty.jetty.servlet.ServletHandler@b0ce8f 13:03:29.383 INFO - Started HttpContext[/wd,/wd]

13:03:29.404 INFO - Started SocketListener on 0.0.0.0:4444 13:03:29.405 INFO - Started org.openqa.jetty.jetty.Server@192b996

Now I’m ready to proceed

Creating a Test

Selenese, the Selenium language, is simple but powerful There’s nothing to prevent you from authoring tests in the traditional manner, with a text editor However, the Selenium IDE is by far the easiest way into testing You can launch it from the Tools window

Once you have the control panel up, you should add an address to the Base URL field This should

be the address against which relative links will work in the system under test You should see a red dot

on a button in the right-hand corner of the IDE control panel It should be depressed, which means the tool is already in record mode

Figure 18–1 shows the IDE as it should be at this point

Trang 22

Figure 18–1 The Selenium IDE control panel

As you can see, I have used the base URL http://localhost/webwoo/ This is the address of an installed instance of the WOO application I’m going to begin my test at

http://localhost/webwoo/?cmd=AddVenue, so I point my browser to that URL Having arrived there, I want to begin with a sanity test The AddVenue page includes the string “no name provided.” I’d like my test to verify this So I right-click on the text in the browser I’m given the option to select a Selenium command 'verifyWebText' You can see this in Figure 18–2

Trang 23

Figure 18–2 Verifying Text on a web page

Meanwhile, Selenium has recorded both of my visits to the page, and my requirement that text be verified You can see this in Figure 18–3

Figure 18–3 The Selenium IDE generates tests

Trang 24

Notice that each command is divided into three parts: command, target, and value These subdivisions are also known as actions, accessors, and assertions Essentially, a command then instructs

the test engine to perform something (an action), somewhere (accessor), and then to confirm a result (assertion)

Now I can return to my WOO web interface, add a venue, confirm some text, add a space, and confirm again Ultimately, I will end up with a runable test case I can run it in the IDE itself by hitting one of the green “play” buttons at the stop of the IDE control panel Failed test commands will be flagged red, and passes flagged green

You can save your test case from the File menu, and rerun it at a later date Or you can export your test as a PHPUnit class To do this, choose Format from the Options menu and select PHPUnit You can see the menu in Figure 18–4

Figure 18–4 Changing the format

Note the log pane at the bottom of the panel You can see a report there from a successful run of the test case Now that I’ve set the correct format, it’s a matter of saving the file As you might expect, you can choose Save As from the File menu Here’s the contents of the saved file:

class Example extends PHPUnit_Extensions_SeleniumTestCase {

function setUp() {

$this->setBrowser("*firefox");

$this->setBrowserUrl("http://localhost/webwoo/");

}

function testMyTestCase() {

$this->open("/webwoo/?cmd=AddVenue");

try {

Trang 25

$this->assertTrue($this->isTextPresent("no name provided"));

} catch (PHPUnit_Framework_AssertionFailedError $e) { array_push($this->verificationErrors, $e->toString());

} $this->type("venue_name", "my_test_venue");

$this->click("//input[@value='submit']");

$this->waitForPageToLoad("30000");

try { $this->assertTrue($this->isTextPresent("'my_test_venue' added"));

} catch (PHPUnit_Framework_AssertionFailedError $e) { array_push($this->verificationErrors, $e->toString());

} $this->type("space_name", "my_test_space");

$this->click("//input[@value='submit']");

$this->waitForPageToLoad("30000");

try { $this->assertTrue($this->isTextPresent("space 'my_test_space' added"));

} catch (PHPUnit_Framework_AssertionFailedError $e) { array_push($this->verificationErrors, $e->toString());

} } }

I changed the default browser from 'chrome' to 'firefox.' Apart from that, I have made no changes at all to this test Remember that I started the Selenium Server a while back This must be running, or PHPUnit tests that use Selenium will fail It is the server that launches the browser (Firefox in this case, though most modern browsers are supported for running tests)

With the test saved and the server running I can execute my test case:

$ phpunit seleniumtest.php

PHPUnit 3.4.11 by Sebastian Bergmann

Time: 11 seconds, Memory: 4.00Mb

OK (1 test, 3 assertions)

If you run the test, not only will you see this output, you’ll see a browser window pop up, invoked by the server, and the actions executed at lightning speed The sort of point and click grunt work that we used to have to do by hand, neatly automated

Of course I’ve only just scratched the surface of Selenium here But hopefully it’s enough to give you

an idea of the possibilities If you want to learn more, there is a complete Selenium manual at http://seleniumhq.org/docs/index.html You should also take a look at the Selenium documentation on the PHPUnit site at http://www.phpunit.de/manual/current/en/selenium.html

A Note of Caution

It’s easy to get carried away with the benefits that automated tests can offer I add unit tests to my projects, and I use PHPUnit for functional tests as well That is, I test at the level of the system as well as that of the class I have seen real and observable benefits, but I believe that these come at a price

Ngày đăng: 14/12/2013, 17:15

TỪ KHÓA LIÊN QUAN