What if I told you that unit testing would save you more time in the long run than it takes to implement in the first place?

What if I told you that functional testing would allow you to detect problems ahead of time, before your users even have a chance to see them?

What if I told you that there's a free set of tools to unlock this amazing power?

You'd be amazed, right?

PHPUnit and the Silex WebTestCase/Symfony BrowserKit components are some of those tools. They can be used to perform both Unit and Functional Testing on Silex codebases.

My ongoing UpThing/Quick Web Apps tutorial demonstrates the process of gradual code improvement, which means that it has some intentional flaws. It's currently a collection of a few web pages with all the logic embedded directly into the lambda function that defines the route. This actually makes it very difficult to do unit testing properly - however, thanks to Silex and the Symfony/Browser-Kit component, it's actually very straightforward to enable some limited functional testing for UpThing. And, as we convert it to take full advantage of functional testing, that will improve the overall code quality and make it easier to unit test the code.

Wire It Up

Functional testing is a broader concept than unit testing - where unit testing concerns itself with testing individual objects/methods (the 'units'), functional testing takes a more user-centric approach and attempts to automatically analyze the cohesion of the system.

Let's adjust the UpThing project to get the tests working.

We'll need to include three dev-only dependencies: phpunit, symfony/browser-kit, and symfony/css-selector. Composer's "--dev" flag ensures that the dependencies are only installed in dev environments; when you deploy to production, you should use the corresponding "--no-dev" flag to reduce the footprint of your installation.

$> composer require --dev phpunit/phpunit "*" $> composer require --dev symfony/browser-kit ">=2.3,<2.4-dev" $> composer require --dev symfony/css-selector "*"

Note: If you have PHPUnit installed on your system globally, you can generally ignore that part and adjust the commands accordingly (use "phpunit" instead of "vendor/bin/phpunit").

Create phpunit.xml.dist, like so:

<?xml version="1.0" encoding="UTF-8"?> <phpunitbackupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" syntaxCheck="false"> <testsuites> <testsuitename="UpThing"> <directory>./tests/</directory> </testsuite> </testsuites> </phpunit>

Adjust bootstrap and index.php to enable test environments:

When I was preparing and researching this blog post, I discovered a small difference between what the Silex "WebTestCase" class expected and what the index.php page returned. In order to accommodate these differences, I decided to implement a quick "Env" state like you would find in a Symfony2 project.

In bootstrap.php, I added this just above the $app['debug'] line:

// Detect environment (default: prod) by checking for the existence of $app_env // (If you know of a safer or smarter way to do this that works with both HTTP // and CLI, let me know) if (isset($app_env) && in_array($app_env, array('prod','dev','test'))) $app['env'] = $app_env; else $app['env'] = 'prod';

Note: $app_env will be set inside our test class, before it includes web/index.php - see createApplication(), below

In web/index.php, I replaced the $app->run(); line with this:

if ('test' == $app['env']) return $app; else $app->run();

The purpose of this is so that I can adjust the ->createApplication() method of the test classes to return a proper HttpKernelInterface-based object, which is what the WebTestCase logic expects.

Create placeholder tests:

It's time to make our first tests. At this point we just want to ensure that all the above stuff worked properly, so we'll create an "Incomplete" test that we'll improve later with actual testing logic. Create a tests/ folder and begin editing tests/WebTest.php - the first step is to create the important bits of the class:

<?php require __DIR__ . '/../vendor/autoload.php'; use Silex\WebTestCase; class WebTest extends WebTestCase { // ... }

Now we will add a ->createApplication() method inside the class, to properly prepare UpThing for testing:

class WebTest extends WebTestCase { public function createApplication() { $app_env = 'test'; return require __DIR__ . '/../web/index.php'; } }

Make sure you use "require" and not "require_once", as future tests will execute ->createApplication() for each call.

Let's add our first test. In PHPUnit test classes, any method starting with the word "test" will be executed as part of the process, so we'll call ours "testUploadForm":

public function testUploadForm() { $this->markTestIncomplete(); }

Now you can save and exit, then go to the root of the project. Run "vendor/bin/phpunit", and you should see output like this:

$> vendor/bin/phpunit PHPUnit 3.7.24-4-g780cdb1 by Sebastian Bergmann. Configuration read from /home/user/projects/upthing/phpunit.xml.dist I Time: 0 seconds, Memory: 6.25Mb OK, but incomplete or skipped tests! Tests: 1, Assertions: 0, Incomplete: 1.

There you go! Now let's make it actually do something.

Make It Work

The next stage of functional testing is to impersonate a browser making a request to the application. In this case, we're going to load the upload form and ensure that it returns HTTP-200 and contains a specific string of text. This is a super simple test that just verifies the site is tied together properly and responding as expected.

Later on, we'll attempt to interact with the form and upload various kinds of acceptable and unacceptable data via the form - there's a reason we can't just jump straight to that, though, and I'll explain it when we get to that stage.

The first step in impersonating a browser is creating a "client":

$client = $this->createClient();

This returns an object of type Symfony\Component\HttpKernel\Client, which can be used to issue Requests (another Symfony object) and Responses (yep, that's an object too). These Requests and Responses are the same things that are generated when a web browser loads the application, but instead of using their "browser output", we'll be intercepting them just prior to that so we can look at what's inside them.

The next step is to submit the Request to our application and return the Response as an instance of "DomCrawler". This will give us the ability to interact with the Response as though we were a regular browser.

$crawler = $client->request('GET','/');

Now that that's out of the way, we can declare our first assertion - this is the meat and potatoes of unit testing, because if an assertion fails to pass, it means that something is seriously wrong in the code. In this case, potential issues could range from a missing dependency to someone breaking the twig template in some unexpected way - anything that results in a non-HTTP-200 result code is deemed to be a "failure condition".

$this->assertEquals( 200, $client->getResponse()->getStatusCode(), 'Upload form failed to load' );

While we're at it, let's check for the presence of the "Initiate Upload" text label using Symfony CSS Selector notation:

$this->assertCount( 1, $crawler->filter('html:contains("Initiate Upload")'), 'Upload button not found' );

At this point, running "vendor/bin/phpunit" will return output like this:

PHPUnit 3.7.24-4-g780cdb1 by Sebastian Bergmann. Configuration read from /home/user/projects/upthing/phpunit.xml.dist . Time: 126 ms, Memory: 14.25Mb OK (1 test, 2 assertions)

And that's what we like to see.

Make It Testable

As I mentioned earlier, UpThing's goal of demonstrating the process of gradual code improvement means that there are some shortcomings, and this directly affects our ability to test the code. You'll encounter this often in older projects that you're updating to take advantage of these new best practices. Most of the PHPUnit guides out there seem to concentrate on testing the 'final form' of idealized code. That may not reflect reality, so you'll have to practice "Maintenance-Driven Development" to improve your codebase.

If we want to test the workflow of the Upload Form, what will happen in its current state? An image will be added to the gallery for each time we run the test. And if we want to test assertions on the gallery page and non-test-related items have been added to the gallery, that could confuse the test suite and generate false errors.

Another issue is - what if we need to make the system scalable in the future? If the code is expected to run on multiple server instances, but access the same images, how will that communication happen between the application and the data storage system? Right now, it's just a folder that sits beside the code. Those images would have to be remotely synced between servers - a fickle and bandwidth-intensive proposition.

There are a few ways to handle this, including just creating a "test_uploads" folder and leaving it at that, but we're going to go one step further: we're going to use a filesystem abstraction layer called Gaufrette to allow us to swap out our entire storage system at the drop of a hat. And because we don't really need to access the test data after the tests have executed, we're also going to use the InMemory Adapter provided by Gaufrette to provide a single-use storage system.

First we require Gaufrette:

$> composer require --dev knplabs/gaufrette "*"

The bootstrap will need to be modified, which I accomplished by inserting this code below the $app_env environment definition we previously added:

// Configuring the filesystem based on environment if ('test' == $app['env']) { $app['upthing.adapter'] = new InMemoryAdapter(); } else { $app['upthing.adapter'] = new LocalAdapter(__DIR__ . '/uploads'); } $app['upthing.filesystem'] = new Filesystem($app['upthing.adapter']); $map = StreamWrapper::getFilesystemMap(); $map->set('upthing', $app['upthing.filesystem']); StreamWrapper::register(); $app['upthing.storage'] = 'gaufrette://upthing/';

Note: I didn't use Silex's lazy loading ($app->share()) to provide the service. This is because we plan on using the stream wrapper, and I'm pretty sure that the stream wrapper cannot be loaded on-demand without specifically calling for it to be registered. Maybe this can be changed later.

I also added this section at the top of bootstrap.php, to properly define the Gaufrette classes being used:

use Gaufrette\Filesystem; use Gaufrette\StreamWrapper; use Gaufrette\Adapter\Local as LocalAdapter; use Gaufrette\Adapter\InMemory as InMemoryAdapter;

Next, we need to change the way we store and access files (lovely!) - in a real-world project, this can be a massive undertaking, so it helps to research and plan the required changes ahead of time. In UpThing, it'll actually need to be changed twice - once to accommodate this testing chapter, and one more time once the database layer is implemented - so the more we can do now to make it efficient, the faster it will be to implement changes in the future.

We're going to register the Gaufrette StreamWrapper to make it easier to read files, but we'll also need to change the way we write files - right now, UpThing uses the $image->move() and $thumbnail->writeImage() methods to add files to the system, but neither of those seem to work with Gaufrette in its current implementation. Luckily, that's easy to solve - all we need to do is read the data into memory (boo!) and use file_put_contents() to write them to disk.

The file upload logic, using $image->move(), becomes similar to this:

$storage_path = $app['upthing.storage']; $new_name = 'img_' . microtime(true); file_put_contents( $storage_path . $new_name, file_get_contents($image->getPathname()) );

And the new thumbnail logic becomes similar to this:

$storage_path = $app['upthing.storage']; $data = $app['imagine']->open($full_name) ->thumbnail( new Imagine\Image\Box($thumb_width,$thumb_height), Imagine\Image\ImageInterface::THUMBNAIL_INSET) ->get('jpg'); file_put_contents($storage_path . $thumb_name, $data);

Essentially what we're doing is this:

file_put_contents('gaufrette://upthing/myNewThumbnail.jpg', $data);

Write the Big Test

Now we write the test. Because this is a functional test, and not a true unit test, we're going to start with a single test case that does a run-though of the whole system (normally you'd have smaller tests that exercise specific pieces of logic). This sort of behaviour is sometimes called a User Story - in this case, our user is going to upload an image and ensure that the image shows up in the gallery and has a thumbnail. So, we'll add these new operations to the testUploadForm function we wrote previously:

public function testUploadForm() { // Set up $test_image = __DIR__ . '/Resources/test_img.jpg'; $client = $this->createClient(); // Test the rendering of the upload form $crawler = $client->request('GET','/'); $this->assertEquals( 200, $client->getResponse()->getStatusCode(), 'Upload form failed to load' ); $this->assertCount( 1, $crawler->filter('html:contains("Initiate Upload")'), 'Upload button not found' ); // Isolate the form so we can test uploads $button = $crawler->selectButton('submit'); $form = $button->form(); // Upload a test image and test that the proper redirect was returned $form['image']->upload($test_image); $client->submit($form); $this->assertTrue( $client->getResponse()->isRedirect('/view'), 'Expected a redirect to the gallery, did not receive it' ); // Test that image shows up in gallery $crawler = $client->followRedirect(); $this->assertEquals( 1, $crawler->filter('div > img.img-thumbnail')->count(), 'Gallery page did not contain expected number of image elements' ); // Extract image URL(s) for testing retrieval $img_url = $crawler->filter('div > img.img-thumbnail')->extract(array('src')); $this->assertCount(1, $img_url, 'Did not return expected number of images'); // Test retrieval of original image $crawler = $client->request('GET', $img_url[0] . '/original'); $md5_test = md5(file_get_contents($test_image)); $md5_result = md5(file_get_contents($client->getResponse()->getFile())); $this->assertEquals( $md5_test, $md5_result, 'MD5 mismatch - the uploaded image did not match the file that was uploaded' ); // Test retrieval of thumbnail (to ensure that processing is working) $crawler = $client->request('GET', $img_url[0]); $this->assertEquals( 200, $client->getResponse()->getStatusCode(), 'Thumbnail retrieval did not return HTTP-200' ); $thumb_data = $client->getResponse()->getContent(); $this->assertGreaterThan( '0', strlen($thumb_data), 'Thumbnail was not properly generated, should be greater than 0 bytes' ); }

In order to get this working, I added a "tests/Resources/test_img.jpg" file, and I modified the upload_form.html.twig file so that the submit button contains 'id="submit"'.

There's a lot of complexity in this test method, and it could use a refactoring of its own. One way to handle things would be to store the "client" in a member variable for the class, and break out the "upload an image" test to its own test case with a data provider, so multiple image uploads could be tested. If you ensure that the upload test runs first, then the gallery test can be adapted to that to ensure that multiple images are working as expected.

When writing your own test cases, you'll also want to test that failure conditions are working properly. For example, you'll want to make a test that uploads invalid data to the form, to ensure that bad input is being rejected.

Tests Passed

Well, that's the end of this beast of a post. I hope you learned something from it. The source code is available on Github (view Branch 0.5 for this article's code), so if you have any questions or confusion, hopefully that will clear things up. If not, you're welcome to contact me on Twitter or you can file an issue against the UpThing project on Github.

The benefit of this tiny test suite is that now we can dig into the code with gusto, rearrange it, refactor it, and know that it still functions as expected. Having that confidence, and that quick ability to test changes, greatly accelerates development and maintenance.

Thanks for checking in!

Continue to Part 5.1 (Travis CI) »

« Back to Quick Web Apps: Part Four (Bootstrap)

View the Source on GitHub »