Testing file uploads with PHP
First published at Thursday 9 December 2010
Warning: This blog post is more then 14 years old – read and use with care.
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.
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: