This blog post has first been published in the Qafoo blog and is duplicated here since I wrote it or participated in writing it.
Cover image for post Testing file uploads with PHP

Testing file uploads with PHP

A question I am asked on a regular basis is, how you can test a file upload with PHP. In this blog post, I take a precise look at this topic and show you how to test your file uploads from within your standard testing environment, using widely unknown testing framework for PHP.

Let me begin with the testing framework that we all know and use in our daily work, PHPUnit. With this testing framework you have the opportunity to test almost every part of your software. This also applies to file uploads, as long as the application under test realizes uploads only via the PHP's magic $_FILES variable. In this case you can easily prepare an appropriately crafted $_FILES array in the test's setUp() method, that can be accessed by the software under test:

// ... protected function setUp() { parent::setUp(); $_FILES = array( 'file_valid' => array( 'name' => 'foo.txt', 'tmp_name' => '/tmp/php42up23', 'type' => 'text/plain', 'size' => 42, 'error' => 0 ) ); } // ...

But fortunately, in most cases this is not quite so simple because the software to be tested utilizes the safer PHP functions is_uploaded_file() and move_uploaded_file(). And in this case the manipulation of the $_FILES array does not work, because both functions operate on another level, so that you cannot manipulate the input data within userland code:

class UploadExample { protected $dest; public function __construct($dest) { $this->dest = rtrim($dest, '/') . '/'; } public function handle($name) { if (is_uploaded_file($_FILES[$name]['tmp_name'])) { move_uploaded_file( $_FILES[$name]['tmp_name'], $this->dest . $_FILES[$name]['name'] ); } } }

Of course, you can still test this part of the application with a heavyweight framework like Selenium. Which, however, brings a number of disadvantages: You must prepare and integrate a variety of other infrastructure components. Beginning with a webserver, the Selenium server, a graphical user interface with a browser and other infrastructure + bootstrap code that is required for a working version of the software. All this increases the required effort to execute a single test and the execution time of the test itself. This carries the danger that false positives arise, caused as a side effects from the complex test infrastructure.

An alternative is the PHP Testing Framework or in short PHPT, which the core developers use to test PHP itself and that is used by various other PHP related projects like PEAR and PECL. I would describe PHPT as a lightweight testing framework, that is easy to learn due to its simple syntax and a very expressive description format. In this article I will only take a look at a limited set of the PHPT syntax. A complete documentation of PHPT can be found on the PHP Quality Assurance website. The following example shows the three base elements of a minimal PHPT test case.

# A description of the test itself:

--TEST-- Example test case

# The code under test(CUT):

--FILE-- <?php var_dump(strpos('Manuel Pichler', 'P')); var_dump(strpos('Manuel Pichler', 'Z'));

# And the test expectations:

--EXPECT-- int(7) bool(false)

But the coolest thing is that, without knowing it, almost everyone has PHPT already installed, because every PEAR installation already contains a PHPT test-runner. This test-runner can be called by the following command:

~ $ pear run-tests example-test-case.phpt Running 1 tests PASS Example test case[example-test-case.phpt] TOTAL TIME: 00:00 1 PASSED TESTS 0 SKIPPED TESTS

After this brief introduction into PHPT, let's come back to the original problem: How to test a file-upload with PHP? Here exactly lies the great strength of PHPT compared to other testing frameworks. With PHPT we can simulate almost every state the application under test could run into. For example, it allows you to alter all php.ini settings, to disable internal functions, or to configure an open_basedir restriction for a test. With PHPT it is also possible to manipulate all data that is passed into PHP process, including the various super global variables like $_FILES, $_POST, $_SERVER, $_ENV. These changes occurres on a different abstraction level than you can get in PHPUnit, so that even internal functions like is_uploaded_file() and move_uploaded_file() operate on the manipulated test data.

In order to test a file-upload one brick is still missing, namely how to simulate an upload with PHPT? For this you need the --POST_RAW-- element, where — as the name implies — a raw HTTP Post message can be specified. The easiest way to get appropriate test data is probably to record a real file-upload with a tool like Wireshark and copy the relevant data into the PHPT test. The following listing shows such a recording:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfywL8UCjFtqUBTQn ------WebKitFormBoundaryfywL8UCjFtqUBTQn Content-Disposition: form-data; name="file"; filename="example.txt" Content-Type: text/plain Qafoo provides quality assurance support and consulting ------WebKitFormBoundaryfywL8UCjFtqUBTQn Content-Disposition: form-data; name="submit" Upload ------WebKitFormBoundaryfywL8UCjFtqUBTQn--

Now you have all information together to test the upload of a file with PHPT. So here is the actual test's description:

--TEST-- Example test emulating a file upload

Then you add the Wireshark recording with the file upload in the --POST_RAW-- element of the PHPT-file:

--POST_RAW-- Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfywL8UCjFtqUBTQn ------WebKitFormBoundaryfywL8UCjFtqUBTQn Content-Disposition: form-data; name="file"; filename="example.txt" Content-Type: text/plain Qafoo provides quality assurance support and consulting ------WebKitFormBoundaryfywL8UCjFtqUBTQn Content-Disposition: form-data; name="submit" Upload ------WebKitFormBoundaryfywL8UCjFtqUBTQn--

Now you just need a little bit glue code to execute the upload component and the actual test assertions:

--FILE-- <?php require __DIR__ . '/UploadExample.php'; $upload = new UploadExample('/tmp'); $upload->handle('file'); var_dump(file_exists('/tmp/example.rst')); ?> --EXPECT-- bool(true)

And that's it. You have a complete PHPT test. Of course, you verify that it works by calling the run-tests command:

~ $ pear run-tests upload-example.phpt Running 1 tests PASS Example test emulating a file upload[upload-example.phpt] TOTAL TIME: 00:00 1 PASSED TESTS 0 SKIPPED TESTS

The test runs and the task has been completed successfully.

Maybe you wonder, how you can integrate these PHPT tests into an existing test infrastructure? A fairly simple solutionexists: A relatively unknown feature of PHPUnit is the built-in support for PHPT tests. This provides the great benefit that you must not worry about the integration, your tests must only inherit from the appropriate test classes:

  • PHPUnit_Extensions_PhptTestCase

  • PHPUnit_Extensions_PhptTestSuite

The PHPUnit_Extensions_PhptTestCase class can be used to reference a single PHPT test file, which is then executed by the PHPUnit testing framework. It has to be noted that the absolute path to the PHPT file must be specified:

<?php require_once 'PHPUnit/Extensions/PhptTestCase.php'; class UploadExampleTest extends PHPUnit_Extensions_PhptTestCase { public function __construct() { parent::__construct(__DIR__ . '/upload-example.phpt'); } }

Alternatively, you can use the PHPUnit_Extensions_PhptTestSuite class, that takes a directory as its first constructor argument and then searches for all *.phpt files within this directory:

<?php require_once 'PHPUnit/Extensions/PhptTestSuite.php'; class UploadExampleTestSuite extends PHPUnit_Extensions_PhptTestSuite { public function __construct() { parent::__construct(__DIR__); } }

Using this powerful combination it should be easy for you to integrate file upload tests into your existing test infrastructure.

As an attachment to this article you can find a more complex example that tests a file-upload within the context for the Zend-Framework. The great advantage of PHPT tests is that such a test can be executed without the complicated path through a webserver and the HTTP protocol.

You can find the full code examples from this blog post in our github repository.