DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Coding
  3. Languages
  4. Testing PHP scripts

Testing PHP scripts

Giorgio Sironi user avatar by
Giorgio Sironi
·
Apr. 25, 12 · Interview
Like (0)
Save
Tweet
Share
10.50K Views

Join the DZone community and get the full member experience.

Join For Free

Let's see a common scenario: you want to refactor some legacy code (without any test), which may not even be object-oriented, into some discrete chunks that you can evolve more easily and test automatically instead of by opening a browser each time.

The legacy code dilemma, however, is always present: you can't refactor the code before putting up some tests on it to avoid regressions and putting your application offline. At the same time, you can't easily unit test the code until some refactoring is introduced.

In the PHP case, it is possibile that a part of the code is clustered in several index.php and script.php files placed in several folders, that do not correspond to a single point of entry but are called directly by the web server. In this article we'll see a non-invasive technique to put them under test and enable refactoring to happen.

Indirection

To test a PHP script, we'll have to reproduce an HTTP request, and check that the returned response is equal to the expected value. Note that response and requests are defined not only by content but also by their headers, when they influence the behavior of the script.

Moreover, we have to stub out the calls made by the PHP script: if we want to test a Transaction Script in isolation we wouldn't let the contained code touch a real database or the rest of the application.

In reality, where there is the need to test PHP scripts directly, usually no one is touching them to avoid breakages. Since they are impossible to test as-is (try executing header() from the command line) without an extension like runkit, I suggest to make a copy of the PHP script so that we can perform some minimal surgery.
There are minimal modifications we have to introduce: remove include and require statements which are already performed by the test suite bootstrap, and change global function calls to some indirection; for example $object->header() instead of header().

The final result we want to obtain is to test a Transaction Script object, which delegates to the copy of the script in its internals. The test will never change after that, and we will be able to extract pieces from the script and putting them into our new object.

How it's done

Faking the HTTP request is performed by redefining the $_GET and $_POST variables, modifying also $_SERVER in case special headers are needed. Even if you overwrite these variable, and even in a command line process like PHPUnit's, they will still be superglobal and visible from the script internal functions.

The response body can be captured via ob_start() and ob_get_clean(), which set up an output buffer to collect every call to echo() or byte stored outside of the <?php tags.

Output buffering supports multiple levels of nesting in PHP, so in most cases this interception will work even if the script uses ob_* calls itself.

The script should be included inside a method of our Transaction Script object, so that the scope of the method will be inherited. For example:

  • variables necessary for the script can be defined as local variables of the wrapper object, e.g. $connection for a database connection.
  • primitives to call instead of the original PHP functions can be defined on the object: $this->header() instead of header(), and same for other functions.

The inclusion-inside-method technique was commonly used to render templates in PHP frameworks like Zend Framework 1.

The end result

This is our Transaction Script object, specific to the script we want to wrap:

<?php
class ForumPosting
{
    private $headers = array();

    public function handleRequest($postRequest)
    {
        $_POST = $postRequest;
        $connection = $this->getAConnection();
        ob_start();
        include 'forum/post_new_copy.php';
        $content = ob_get_clean();
        return array(
            'content' => $content,
            'headers' => $this->headers
        );
    }

    private function header($headerLine)
    {
        $this->headers[] = $headerLine;
    }
    
    ...
}

And this is our test:

public function testANewPostIsCreated()
{
    $action = new ForumPosting();
    $response = $action->handleRequest(array(
        'id_thread' => 42,
        'text' => 'Hello, world',
        ...
    ));
    $this->assertEquals('...', $response['content']);
    $this->assertContains('Content-type: text/html', $response['headers']);
}

Conclusion

This hack is only temporary! It makes us able to write tests which will not change anymore, at the acceptance level; we put .php scripts under test to make these tests pass, and then refactor the scripts to eliminate the cruft.

When we're finished, handleRequest() will contain just the real logic instead of an inclusion. If you have many such scripts to test, you may develop a generic wrapper object to suit your needs to use inside Transaction Script objects. In any case, when PHP scripts are complex don't start to tear them apart before setting up tests like these: regressions are very easy to introduce and never catch.

PHP unit test

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Specification by Example Is Not a Test Framework
  • OpenVPN With Radius and Multi-Factor Authentication
  • Journey to Event Driven, Part 1: Why Event-First Programming Changes Everything
  • Create a CLI Chatbot With the ChatGPT API and Node.js

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: