|
| 1 | +Create your own framework... on top of the Symfony2 Components (part 8) |
| 2 | +======================================================================= |
| 3 | + |
| 4 | +Some watchful readers pointed out some subtle but nonetheless important bugs |
| 5 | +in the framework we have built yesterday. When creating a framework, you must |
| 6 | +be sure that it behaves as advertised. If not, all the applications based on |
| 7 | +it will exhibit the same bugs. The good news is that whenever you fix a bug, |
| 8 | +you are fixing a bunch of applications too. |
| 9 | + |
| 10 | +Today's mission is to write unit tests for the framework we have created by |
| 11 | +using `PHPUnit`_. Create a PHPUnit configuration file in |
| 12 | +``example.com/phpunit.xml.dist``: |
| 13 | + |
| 14 | +.. code-block:: xml |
| 15 | +
|
| 16 | + <?xml version="1.0" encoding="UTF-8"?> |
| 17 | +
|
| 18 | + <phpunit backupGlobals="false" |
| 19 | + backupStaticAttributes="false" |
| 20 | + colors="true" |
| 21 | + convertErrorsToExceptions="true" |
| 22 | + convertNoticesToExceptions="true" |
| 23 | + convertWarningsToExceptions="true" |
| 24 | + processIsolation="false" |
| 25 | + stopOnFailure="false" |
| 26 | + syntaxCheck="false" |
| 27 | + bootstrap="vendor/.composer/autoload.php" |
| 28 | + > |
| 29 | + <testsuites> |
| 30 | + <testsuite name="Test Suite"> |
| 31 | + <directory>./tests</directory> |
| 32 | + </testsuite> |
| 33 | + </testsuites> |
| 34 | + </phpunit> |
| 35 | +
|
| 36 | +This configuration defines sensible defaults for most PHPUnit settings; more |
| 37 | +interesting, the autoloader is used to bootstrap the tests, and tests will be |
| 38 | +stored under the ``example.com/tests/`` directory. |
| 39 | + |
| 40 | +Now, let's write a test for "not found" resources. To avoid the creation of |
| 41 | +all dependencies when writing tests and to really just unit-test what we want, |
| 42 | +we are going to use `test doubles`_. Test doubles are easier to create when we |
| 43 | +rely on interfaces instead of concrete classes. Fortunately, Symfony2 provides |
| 44 | +such interfaces for core objects like the URL matcher and the controller |
| 45 | +resolver. Modify the framework to make use of them:: |
| 46 | + |
| 47 | + <?php |
| 48 | + |
| 49 | + // example.com/src/Simplex/Framework.php |
| 50 | + |
| 51 | + namespace Simplex; |
| 52 | + |
| 53 | + // ... |
| 54 | + |
| 55 | + use Symfony\Component\Routing\Matcher\UrlMatcherInterface; |
| 56 | + use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; |
| 57 | + |
| 58 | + class Framework |
| 59 | + { |
| 60 | + protected $matcher; |
| 61 | + protected $resolver; |
| 62 | + |
| 63 | + public function __construct(UrlMatcherInterface $matcher, ControllerResolverInterface $resolver) |
| 64 | + { |
| 65 | + $this->matcher = $matcher; |
| 66 | + $this->resolver = $resolver; |
| 67 | + } |
| 68 | + |
| 69 | + // ... |
| 70 | + } |
| 71 | + |
| 72 | +We are now ready to write our first test:: |
| 73 | + |
| 74 | + <?php |
| 75 | + |
| 76 | + // example.com/tests/Simplex/Tests/FrameworkTest.php |
| 77 | + |
| 78 | + namespace Simplex\Tests; |
| 79 | + |
| 80 | + use Simplex\Framework; |
| 81 | + use Symfony\Component\HttpFoundation\Request; |
| 82 | + use Symfony\Component\Routing\Exception\ResourceNotFoundException; |
| 83 | + |
| 84 | + class FrameworkTest extends \PHPUnit_Framework_TestCase |
| 85 | + { |
| 86 | + public function testNotFoundHandling() |
| 87 | + { |
| 88 | + $framework = $this->getFrameworkForException(new ResourceNotFoundException()); |
| 89 | + |
| 90 | + $response = $framework->handle(new Request()); |
| 91 | + |
| 92 | + $this->assertEquals(404, $response->getStatusCode()); |
| 93 | + } |
| 94 | + |
| 95 | + protected function getFrameworkForException($exception) |
| 96 | + { |
| 97 | + $matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); |
| 98 | + $matcher |
| 99 | + ->expects($this->once()) |
| 100 | + ->method('match') |
| 101 | + ->will($this->throwException($exception)) |
| 102 | + ; |
| 103 | + $resolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface'); |
| 104 | + |
| 105 | + return new Framework($matcher, $resolver); |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | +This test simulates a request that does not match any route. As such, the |
| 110 | +``match()`` method returns a ``ResourceNotFoundException`` exception and we |
| 111 | +are testing that our framework converts this exception to a 404 response. |
| 112 | + |
| 113 | +Executing this test is as simple as running ``phpunit`` from the |
| 114 | +``example.com`` directory: |
| 115 | + |
| 116 | +.. code-block:: bash |
| 117 | +
|
| 118 | + $ phpunit |
| 119 | +
|
| 120 | +After the test ran, you should see a green bar. If not, you have a bug |
| 121 | +either in the test or in the framework code! |
| 122 | + |
| 123 | +Adding a unit test for any exception thrown in a controller is just as easy:: |
| 124 | + |
| 125 | + public function testErrorHandling() |
| 126 | + { |
| 127 | + $framework = $this->getFrameworkForException(new \RuntimeException()); |
| 128 | + |
| 129 | + $response = $framework->handle(new Request()); |
| 130 | + |
| 131 | + $this->assertEquals(500, $response->getStatusCode()); |
| 132 | + } |
| 133 | + |
| 134 | +Last, but not the least, let's write a test for when we actually have a proper |
| 135 | +Response:: |
| 136 | + |
| 137 | + use Symfony\Component\HttpFoundation\Response; |
| 138 | + use Symfony\Component\HttpKernel\Controller\ControllerResolver; |
| 139 | + |
| 140 | + public function testControllerResponse() |
| 141 | + { |
| 142 | + $matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); |
| 143 | + $matcher |
| 144 | + ->expects($this->once()) |
| 145 | + ->method('match') |
| 146 | + ->will($this->returnValue(array( |
| 147 | + '_route' => 'foo', |
| 148 | + 'name' => 'Fabien', |
| 149 | + '_controller' => function ($name) { |
| 150 | + return new Response('Hello '.$name); |
| 151 | + } |
| 152 | + ))) |
| 153 | + ; |
| 154 | + $resolver = new ControllerResolver(); |
| 155 | + |
| 156 | + $framework = new Framework($matcher, $resolver); |
| 157 | + |
| 158 | + $response = $framework->handle(new Request()); |
| 159 | + |
| 160 | + $this->assertEquals(200, $response->getStatusCode()); |
| 161 | + $this->assertContains('Hello Fabien', $response->getContent()); |
| 162 | + } |
| 163 | + |
| 164 | +In this test, we simulate a route that matches and returns a simple |
| 165 | +controller. We check that the response status is 200 and that its content is |
| 166 | +the one we have set in the controller. |
| 167 | + |
| 168 | +To check that we have covered all possible use cases, run the PHPUnit test |
| 169 | +coverage feature (you need to enable `XDebug`_ first): |
| 170 | + |
| 171 | +.. code-block:: bash |
| 172 | +
|
| 173 | + phpunit --coverage-html=cov/ |
| 174 | +
|
| 175 | +Open ``example.com/cov/src_Simplex_Framework.php.html`` in a browser and check |
| 176 | +that all the lines for the Framework class are green (it means that they have |
| 177 | +been visited when the tests were executed). |
| 178 | + |
| 179 | +Thanks to the simple object-oriented code that we have written so far, we have |
| 180 | +been able to write unit-tests to cover all possible use cases of our |
| 181 | +framework; test doubles ensured that we were actually testing our code and not |
| 182 | +Symfony2 code. |
| 183 | + |
| 184 | +Now that we are confident (again) about the code we have written, we can |
| 185 | +safely think about the next batch of features we want to add to our framework. |
| 186 | + |
| 187 | +.. _`PHPUnit`: http://www.phpunit.de/manual/current/en/index.html |
| 188 | +.. _`test doubles`: http://www.phpunit.de/manual/current/en/test-doubles.html |
| 189 | +.. _`XDebug`: http://xdebug.org/ |
0 commit comments