Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Advanced PHP Programming

.pdf
Скачиваний:
71
Добавлен:
14.04.2015
Размер:
7.82 Mб
Скачать

148 Chapter 5 Implementing with PHP: Standalone Scripts

if($service->current_status() !== $service->previous_status()) { if($service->current_status() === ServiceCheck::FAILURE) {

$status = DOWN;

}

else {

$status = UP;

}

error_log({$service->description()} changed status to $status);

}

}

public function log_current_status(ServiceCheck $service)

{

error_log({$service->description()}: $status);

}

}

The log_current_status() method means that if the process is sent a SIGUSR1 signal, it dumps the complete current status to your PHP error log.

The engine takes a configuration file like the following:

<config>

<loggers>

<logger>

<id>errorlog</id> <class>ErrorLog_ServiceLogger</class>

</logger>

<logger>

<id>emailme</id> <class>EmailMe_ServiceLogger</class>

</logger>

</loggers>

<services>

<service> <class>HTTP_ServiceCheck</class> <params>

<description>OmniTI HTTP Check</description> <url>http://www.omniti.com</url> <timeout>30</timeout> <frequency>900</frequency>

</params>

<loggers>

<logger>errorlog</logger>

<logger>emailme</logger>

</loggers>

</service>

<service>

Combining What You’ve Learned: Monitoring Services

149

<class>HTTP_ServiceCheck</class> <params>

<description>Home Page HTTP Check</description> <url>http://www.schlossnagle.org/~george</url> <timeout>30</timeout> <frequency>3600</frequency>

</params>

<loggers>

<logger>errorlog</logger>

</loggers>

</service>

</services>

</config>

When passed this XML file, the ServiceCheckRunner constructor instantiates a logger for each specified logger.Then it instantiates a ServiceCheck object for each specified service.

Note

The constructor uses the Reflection_Class class to introspect the service and logger classes before you try to instantiate them. This is not necessary, but it is a nice demonstration of the new Reflection API in PHP 5. In addition to classes, the Reflection API provides classes for introspecting almost any internal entity (class, method, or function) in PHP.

To use the engine you’ve built, you still need some wrapper code.The monitor should prohibit you from starting it twice—you don’t need double messages for every event. It should also accept some options, including the following:

Option

Description

[-f]

A location for the engine’s configuration file, which defaults to moni-

 

tor.xml.

[-n]

The size of the child process pool the engine will allow, which defaults

 

to 5.

[-d]

A flag to disable the engine from daemonizing.This is useful if you

 

write a debugging ServiceLogger process that outputs information to

 

stdout or stderr.

Here is the finalized monitor script, which parses options, guarantees exclusivity, and runs the service checks:

require_once Service.inc; require_once Console/Getopt.php;

$shortoptions = n:f:d;

$default_opts = array(n=> 5, f=> monitor.xml);

150Chapter 5 Implementing with PHP: Standalone Scripts

$args = getOptions($default_opts, $shortoptions, null);

$fp = fopen(/tmp/.lockfile, a);

if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) { fputs($stderr, Failed to acquire lock\n);

exit;

}

if(!$args[d]) { if(pcntl_fork()) {

exit;

}

posix_setsid(); if(pcntl_fork()) {

exit;

}

}

fwrite($fp, getmypid()); fflush($fp);

$engine = new ServiceCheckRunner($args[f], $args[n]); $engine->loop();

Notice that this example uses the custom getOptions() function defined earlier in this chapter to make life simpler regarding parsing options.

After writing an appropriate configuration file, you can start the script as follows:

> ./monitor.php -f /etc/monitor.xml

This daemonizes and continues monitoring until the machine is shut down or the script is killed.

This script is fairly complex, but there are still some easy improvements that are left as an exercise to the reader:

nAdd a SIGHUP handler that reparses the configuration file so that you can change the configuration without restarting the server.

nWrite a ServiceLogger that logs to a database for persistent data that can be queried.

nWrite a Web front end to provide a nice GUI to the whole monitoring system.

Further Reading

There are not many resources for shell scripting in PHP. Perl has a much longer heritage of being a useful language for administrative tasks. Perl for Systems Administration by David N. Blank-Edelman is a nice text, and the syntax and feature similarity between Perl and PHP make it easy to port the book’s Perl examples to PHP.

Further Reading

151

php|architect, an electronic (and now print as well) periodical, has a good article by Marco Tabini on building interactive terminal-based applications with PHP and the ncurses extension in Volume 1, Issue 12. php|architect is available online at http://www.phparch.com.

Although there is not space to cover it here, PHP-GTK is an interesting project aimed at writing GUI desktop applications in PHP, using the GTK graphics toolkit. Information on PHP-GTK is available at http://gtk.php.net.

A good open-source resource monitoring system is Nagios, available at http://nagios.org.The monitoring script presented in this chapter was inspired by Nagios and designed to allow authoring of all your tests in PHP in an integrated fashion. Also, having your core engine in PHP makes it easy to customize your front end. (Nagios is written in C and is CGI based, making customization difficult.)

6

Unit Testing

TESTING AND ENGINEERING ARE INEXTRICABLY TIED FOREVER.

All code is tested at some point—perhaps during its implementation, during a dedicated testing phase, or when it goes live. Any developer who has launched broken code live knows that it is easier to test and debug code during development than after it goes into production.

Developers give many excuses for not testing code until it is too late.These are some of the popular ones:

nThe project is too rushed.

nMy code always works the first time.

nThe code works on my machine.

Let’s explore these excuses. First, projects are rushed because productivity lags. Productivity is directly proportional to the amount of debugging required to make code stable and working. Unfortunately, testing early and testing late are not equal cost operations.The problem is two-fold:

nIn a large code base that does not have a formalized testing infrastructure, it is hard to find the root cause of a bug. It’s a needle-in-a-haystack problem. Finding a bug in a 10-line program is easy. Finding a bug in 10,000 lines of included code is a tremendous effort.

nAs the code base grows, so do the number of dependencies between components. Seemingly innocuous changes to a “core” library—whether adding additional features or simply fixing a bug—may unintentionally break other portions of the application.This is known as refactoring. As the size and complexity of software grow, it becomes increasingly difficult to make these sorts of changes without incurring time costs and introducing new bugs.

nAll software has bugs. Any developer who claims that his or her software is always bug-free is living in a fantasy world.

154 Chapter 6 Unit Testing

nSystem setups are all slightly different, often in ways that are hard to anticipate. Differing versions of PHP, differing versions of libraries, and different file system layouts are just a few of the factors that can cause code that runs perfectly on one machine to inexplicably fail on another.

Although there are no silver bullets to solve these problems, a good unit-testing infrastructure comes pretty close. A unit is a small section of code, such as a function or class method. Unit testing is a formalized approach to testing in which every component of an application (that is, every unit) has a set of tests associated with it.With an automated framework for running these tests, you have a way of testing an application constantly and consistently, which allows you to quickly identify functionality-breaking bugs and to evaluate the effects of refactoring on distant parts of the application. Unit testing does not replace full application testing; rather, it is a complement that helps you create more stable code in less time.

By creating persistent tests that you carry with the library for its entire life, you can easily refactor your code and guarantee that the external functionality has not inadvertently changed. Any time you make an internal change in the library, you rerun the test suite. If the tests run error-free, the refactoring has been successful.This makes debugging vague application problems easier. If a library passes all its tests (and if its test suite is complete), it is less suspicious as a potential cause for a bug.

Note

Unit testing tends to be associated with the Extreme Programming methodology. In fact, pervasive unit testing is one of the key tenets of Extreme Programming. Unit testing existed well before Extreme Programming, however, and can certainly be used independently of it. This book isn’t about singling out a particular methodology as the “one true style,” so it looks at unit testing as a standalone technique for designing and building solid code. If you have never read anything about Extreme Programming, you should check it out. It is an interesting set of techniques that many professional programmers live by. More information is available in the “Further Reading” section at the end of the chapter.

An Introduction to Unit Testing

To be successful, a unit testing framework needs to have certain properties, including the following:

nAutomated—The system should run all the tests necessary with no interaction from the programmer.

nEasy to write—The system must be easy to use.

nExtensible—To streamline efforts and minimize duplication of work, you should be able to reuse existing tests when creating new ones.

An Introduction to Unit Testing

155

To actually benefit from unit testing, we need to make sure our tests have certain properties:

nComprehensive—Tests should completely test all function/class APIs.You should ensure not only that the function APIs work as expected, but also that they fail correctly when improper data is passed to them. Furthermore, you should write tests for any bugs discovered over the life of the library. Partial tests leave holes that can lead to errors when refactoring or to old bugs reappearing.

nReusable—Tests should be general enough to usefully test their targets again and again.The tests will be permanent fixtures that are maintained and used to verify the library over its entire life span.

Writing Unit Tests for Automated Unit Testing

For the testing framework discussed in this chapter, we will use PEAR’s PHPUnit. PHPUnit, like most of the free unit testing frameworks, is based closely on JUnit, Erich Gamma and Kent Beck’s excellent unit testing suite for Java.

Installing PHPUnit is just a matter of running the following (which most likely needs root access):

# pear install phpunit

Alternatively, you can download PHPUnit from http://pear.php.net/PHPUnit.

Writing Your First Unit Test

A unit test consists of a collection of test cases. A test case is designed to check the outcome of a particular scenario.The scenario can be something as simple as testing the result of a single function or testing the result of a set of complex operations.

A test case in PHPUnit is a subclass of the PHPUnit_Framework_TestCase class. An instance of PHPUnit_Framework_TestCase is one or several test cases, together with optional setup and tear-down code.

The simplest test case implements a single test. Let’s write a test to validate the behavior of a simple email address parser.The parser will break an RFC 822 email address into its component parts.

class EmailAddress { public $localPart; public $domain; public $address;

public function _ _construct($address = null) { if($address) {

$this->address = $address; $this->extract();

}

}

156 Chapter 6 Unit Testing

protected function extract() {

list($this->localPart, $this->domain) = explode(@, $this->address);

}

}

To create a test for this, you create a TestCase class that contains a method that tests that a known email address is correctly broken into its components:

require_once EmailAddress.inc;

require_once PHPUnit/Framework/TestClass.php;

class EmailAddressTest extends PHPUnit_Framework_TestCase { public function _ _constructor($name) {

parent::_ _constructor($name);

}

function testLocalPart() {

$email = new EmailAddress(george@omniti.com);

// check that the local part of the address is equal to george$this->assertTrue($email->localPart == george);

}

}

Then you need to register the test class.You instantiate a PHPUnit_Framework_ TestSuite object and the test case to it:

require_omce PHPUnit/Framework/TestSuite;

$suite = new PHPUnit_Framework_TestSuite(); $suite->addTest(new EmailAddressTest(testLocalPart));

After you have done this, you run the test:

require_once PHPUnit/TextUI/TestRunner;

PHPUnit_TextUI_TestRunner::run($suite);

You get the following results, which you can print:

PHPUnit 1.0.0-dev by Sebastian Bergmann.

.

Time: 0.00156390666962

OK (1 test)

Adding Multiple Tests

When you have a number of small test cases (for example, when checking that both the local part and the domain are split out correctly), you can avoid having to create a huge

Writing Inline and Out-of-Line Unit Tests

157

number of TestCase classes.To aid in this, a TestCase class can support multiple tests:

class EmailAddressTestCase extends PHPUnit_Framework_TestCase{

public function _ _constructor($name) { parent::_ _constructor($name);

}

public function testLocalPart() {

$email = new EmailAddress(george@omniti.com);

// check that the local part of the address is equal to george$this->assertTrue($email->localPart == george);

}

public function testDomain() {

$email = new EmailAddress(george@omniti.com); $this->assertEquals($email->domain, omniti.com);

}

}

Multiple tests are registered the same way as a single one:

$suite = new PHPUnit_FrameWork_TestSuite(); $suite->addTest(new EmailAddressTestCase(testLocalPart)); $suite->addTest(new EmailAddressTestCase(testDomain));

PHPUnit_TextUI_TestRunner::run($suite);

As a convenience, if you instantiate the PHPUnit_Framework_TestSuite object with the name of the TestCase class, $suite automatically causes any methods whose names begin with test to automatically register:

$suite = new PHPUnit_Framework_TestSuite(EmailAddressTestCase); // testLocalPart and testDomain are now auto-registered PHPUnit_TextUI_TestRunner::run($suite);

Note that if you add multiple tests to a suite by using addTest, the tests will be run in the order in which they were added. If you autoregister the tests, they will be registered in the order returned by get_class_methods() (which is how TestSuite extracts the test methods automatically).

Writing Inline and Out-of-Line Unit Tests

Unit tests are not only useful in initial development, but throughout the full life of a project. Any time you refactor code, you would like to be able to verify its correctness by running the full unit test suite against it. How do you best arrange unit tests so that they are easy to run, keep up-to-date, and carry along with the library?

There are two options for packaging unit tests. In the first case, you can incorporate your testing code directly into your libraries.This helps ensure that tests are kept up-to- date with the code they are testing, but it also has some drawbacks.The other option is to package your tests in separate files.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]