Qafoo GmbH - passion for software quality

Help you and your team benefit from new perspectives on cutting-edge quality engineering techniques and tools through the Qafoo team weblog.

By Manuel Pichler, first published at Thu, 09 Dec 2010 09:37:11 +0100

Download our free e-book "Crafting Quality Software" with a selection of the finest blog posts as PDF or EPub.

Crafting Quality Software

You can also buy a printed version of the book on Amazon or on epubli.

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:

Get 20% discount on PHP OOP/OOD trainings in January!

// ... 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'] ); } } }

Need a PHP software quality expert? Qafoo provides consulting and training in this area!

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

Do you want to get started with automated software testing efficiently? Qafoo can train your development crew to get the job done right!

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

Problems with PHP QA tools? Qafoo offers technical support for many of them!

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

Got other difficult test scenarios? Rent a Qafoo expert to assist you!

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.

Download our free e-book "Crafting Quality Software" with a selection of the finest blog posts as PDF or EPub.

Crafting Quality Software

You can also buy a printed version of the book on Amazon or on epubli.

Get Technical Insights With Our Newsletter

Stay up to date with regular new technological insights by subscribing to our newsletter. We will send you articles to improve your developments skills.

Comments

  • Tyrael on Thu, 09 Dec 2010 15:14:44 +0100

    thanks, good article.
    I've just wanted to mention, that alternatively you can use runkit, or namespace monkey patching to overwrite functions like is_uploaded_file()
    for example:


    <?php
    namespace monkey {
        function is_uploaded_file($filename){
            return true;
        }

        require __DIR__.'/../classes/MyClass.php';
        class MyClassTest extends \PHPUnit_Framework_TestCase
        {
            public function testAdd()
            {
                $this->assertEquals(2, \MyClass::add(1,1));
            }

            public function testUploadedFile(){
                $this->assertTrue(is_uploaded_file('foo'));
            }
        }
    }

    Tyrael

  • Sebastian Bergmann on Thu, 09 Dec 2010 22:56:31 +0100

    Instead of Runkit you may want to have a look at https://github.com/sebastianbergmann/php-test-helpers

  • Jean-Marc Fontaine on Thu, 09 Dec 2010 23:26:41 +0100

    Internet is great! Yesterday, I was asking myself how to unit test file uploads and the next day you publish this article. :)

    Thanks!

  • Tyrael on Fri, 10 Dec 2010 14:07:32 +0100

    Thanks Sebastian, I missed the rename_function method in your extension.

    Tyrael

  • Ralf on Tue, 14 Dec 2010 10:51:35 +0100

    Hello,

    using extensions to redefine function names is considered as anti pattern "The Local Hero" here: http://blog.james-carr.org/2006/11/03/tdd-anti-patterns/

    Would'nt it be better to wrap an API around these build-in PHP functions?

    Ralf

  • Tyrael on Tue, 14 Dec 2010 16:12:47 +0100

    I can't see the renaming to be explicitly mentioned here.
    you could configure your test cases to check for the required dependency and skip them, if isn't available.

    of course you can put in some effort and refactor the code to be more easy to test, hence you can spare yourself from the above mentioned techniques.

    Tyrael

  • Thomas H on Wed, 15 Dec 2010 11:33:16 +0100

    Hello,
    I need to test an upload file and I am really interested in your solution. Unluckily I have problems with it.

    I'm working under zend framework and test with PHPUnit. I tried the simple example and it works prefectly. My problems begin with the file. I track package with Wireshark and get :

    <code>
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0
    Content-Disposition: form-data; name="Filename"

    example-text.phtml
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0
    Content-Disposition: form-data; name="folder"

    /admin/configuration/
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0
    Content-Disposition: form-data; name="Filedata"; filename="example-text.phtml"
    Content-Type: application/octet-stream

    Ceci est un example
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0
    Content-Disposition: form-data; name="Upload"

    Submit Query
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0--
    </code>

    My test is a part of TestSuite but it is not a problem cause I declare directly the PHPT object in it. Here is my test :
    <code>
    public function testUploadLayout()
        {
            $phpt = new PHPUnit_Extensions_PhptTestCase(
                __DIR__.'/upload-layout.phpt', array('cgi' => 'php-cgi')
            );
            $result = $phpt->run();
            $this->assertTrue($result->wasSuccessful());
        }
    </code>

    This work with the simple example but not with upload.
    $result->errors() returns me an empty array and $result->failures returns something which hasn't end with the tracking of all my zend structure. I don't understand anything.

    My phpt file :
    <code>
    --TEST--
    Example test emulating a file upload
    --POST_RAW--
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0
    Content-Disposition: form-data; name="Filename"

    example-text.phtml
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0
    Content-Disposition: form-data; name="folder"

    /admin/configuration/
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0
    Content-Disposition: form-data; name="Filedata"; filename="example-text.phtml"
    Content-Type: application/octet-stream

    Ceci est un example
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0
    Content-Disposition: form-data; name="Upload"

    Submit Query
    ------------ae0GI3ae0ei4gL6Ij5KM7cH2GI3ae0--
    --FILE--
    <?php
    echo 'test';
    ?>
    --EXPECT--
    test
    </code>

    It doesn't work either with a copy paste of your example.

    I will be very grateful if you find anything to help me.

    Thank you.

    Thomas

  • Matt Schuckmann on Fri, 02 Mar 2012 23:51:18 +0100

    I can't get the upload_example.phpt test to work at all.
    For one in order for the --POST_RAW-- to even have a chance at working in the plain old pear run-tests environment you must included the --cgi=PHPCGI option like so
    pear run-tests --cgi=PHPCGI upload-example.phpt

    However, even with this option the test always.

    Furthermore when I try run it through phpunit cli I must explicitly tell it to run the Alltests.php script or else it confused by UploadExampleTest (UploadExampleTest.php) which does not derive from PHPUnit_Framework_TestCase. And even if I run Alltests.php the tests are just skipped, presumably because the cgi environment is not setup.

    There really isn't much info out there on this stuff, has anyone gotten this to work?

  • Glenn on Sun, 09 Feb 2014 21:23:25 +0100

    After searching for an answer to how to test file uploads in zend framework using phpunit, I eventually found this article and thought, 'Great, finally, I've found the answer!'.

    Well, I've got as far as downloading Wireshark and now I'm stuck. From what I can see, there's nothing in the Wireshark manual to tell me how to capture --POST_RAW-- data. And nothing from my initial playing with the software seems to produce anything remotely resembling the data you have above.

    As this is a key feature of this article, some pointers would be appreciated! :)

  • Alex on Mon, 29 Sep 2014 05:43:14 +0200

    You can capture the POST message using web-browser with Chromium Developer Tools - go to the Network section where request to the submitted page will be listed. Click the request and the complete message will be listed there.

  • Rasmus Schultz on Wed, 03 May 2017 08:11:11 +0200

    Unless you're writing your own implementation of PSR-7, you shouldn't need this. Use PSR-7 and all of this is neatly testable - someone else has done the hard work of testing their PSR-7 implementation so that you will never need to worry or struggle with these kinds of tests again :-)

  • Oliver Russell on Mon, 12 Mar 2018 14:24:09 +0100

    While adding PHP file upload function (https://www.cloudways.com/blog/the-basics-of-file-upload-in-php/ ) to your website, it is also necessary to validate the file type and other things. Also, check for any sql injection vulnerabilities. Forms like these tend to have such vulnerabilities.