I started my new job last year, after a few good years of working within an operational department which involved mainly bug fixing. The new job is more of a greenfield development than bug fixing. This meant an opportunity to apply what I’ve learnt over the past few years and strategically choose a framework that’s both fast and simple to use. At that time, Phalcon v1.2.3 had just made it to master and we decided it was the right framework for what we needed.
Project development started and the work completed along with it’s unit test. As part of this I made my first public git commit. My first contribution to the PHP community in general, and Phalcon folks in particular, was the introduction of MVC Functional Testing with PHPUnit, this meant that Applications developed using Phalcon Framework could be unit tested by requesting a url then asserting that the response is handled by a specific action within the controller. I was very pleased to see the pull request getting merged into Phalcon Incubator. Furthermore, seeing it being maintained by other developers meant it was being used and continues to be cared for.
To date, my latest addition is an example of how to unit test code that utilises Phalcon models, mainly the various static find methods. The code is available on github and the coverage report is coveralls.io.
The reason behind this work was due to the fact that we were either; wrapping these static find methods by a protected function or completely avoiding unit testing these methods. Both of these unsettled me. Now that I’ve managed to get this done, I’m looking forward to ploughing through the backlog and get those lines even greener.
Enough said, let’s step into the code
In order to unit test models without a database connection, we need to convince the model manager that we have our table meta data cached. To do this, I had to use ‘Phalcon\Mvc\Model\MetaData\Files’ for the ‘modelsMetadata’ service. This meant that I could use the same cache files generated by Phalcon, therefore Phalcon doesn’t need to interrogate the information schema for the table in question.
The model under test is for the table below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | CREATE TABLE Popup(Id INT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,Name VARCHAR(24) NOT NULL,Conent VARCHAR(200) NOT NULL,CreatedAt DATETIME NOT NULL,LastUpdate TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL);CREATE INDEX idxName ON Popup ( Name ); |
Phalcon generated a couple of cache files for this table:
tests/resources/map-silverbadge_models_popup.php
1 2 3 4 5 6 7 | <?php return array (0 => NULL,1 => NULL,); |
tests/resources/meta-silverbadge_models_popup-testpopup.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | <?php return array (0 =>array (0 => 'Id',1 => 'Name',2 => 'Conent',3 => 'CreatedAt',4 => 'LastUpdate',),1 =>array (0 => 'Id',),2 =>array (0 => 'Name',1 => 'Conent',2 => 'CreatedAt',3 => 'LastUpdate',),3 =>array (0 => 'Id',1 => 'Name',2 => 'Conent',3 => 'CreatedAt',4 => 'LastUpdate',),4 =>array ('Id' => 0,'Name' => 2,'Conent' => 2,'CreatedAt' => 4,'LastUpdate' => 1,),5 =>array ('Id' => true,),8 => 'Id',9 =>array ('Id' => 1,'Name' => 2,'Conent' => 2,'CreatedAt' => 2,'LastUpdate' => 2,),10 =>array (),11 =>array (),); |
As you can see both of the files now live in the tests/resources directory and will need to be maintained should the table’s structure change. In my opinion, this is more of a best practice than an overhead. An alternative to this is to use ‘Annotations Strategy’, however this may add more development time and can easily get inconsistent with the table structure in the future.
The model under test is below:
src/Silverbadge/Models/Popup.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | <?phpnamespace Silverbadge\Models;use Phalcon\Mvc\Model;class Popup extends Model{/**** @var integer*/protected $Id;/**** @var string*/protected $Name;/**** @var string*/protected $Content;/**** @var string*/protected $CreatedAt;/**** @var string*/protected $LastUpdate;/*** @param string $Content** @return Popup*/public function setContent($Content){$this->Content = $Content;return $this;}/*** @return string*/public function getContent(){return $this->Content;}/*** @param string $Name** @return Popup*/public function setName($Name){$this->Name = $Name;return $this;}/*** @return string*/public function getName(){return $this->Name;}/*** @param string $LastUpdate** @return Popup*/public function setLastUpdate($LastUpdate){$this->LastUpdate = $LastUpdate;return $this;}/*** @return string*/public function getLastUpdate(){return $this->LastUpdate;}/*** @param int $Id** @return Popup*/public function setId($Id){$this->Id = $Id;return $this;}/*** @return int*/public function getId(){return $this->Id;}/*** @param string $CreatedAt** @return Popup*/public function setCreatedAt($CreatedAt){$this->CreatedAt = $CreatedAt;return $this;}/*** @return string*/public function getCreatedAt(){return $this->CreatedAt;}public function getSchema(){return 'test';}public function getSource(){return 'Popup';}/*** Initialize method for model.*/public function initialize(){$this->useDynamicUpdate(true);}} |
And the bit of code that uses the model is below, this single method class has been prepared to illustrate how to unit test both Popup::find() and the save method, nothing overtly complex for this class.
src/Silverbadge/Api/Facade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | <?php/*** Facade.php** @author Rami*/namespace Silverbadge\Api;use Phalcon\Mvc\User\Component;use Silverbadge\Models\Popup;class Facade extends Component{public function createNewPopup($name, $content){$response = array();$content = strip_tags($content);if (empty($content) || empty($name) || count(Popup::find(array("Name = '$name'"))->toArray()) > 0) {$response['code'] = 400;$response['message'] = "Bad request, empty parameters supplied or popup with this name already exists";} else {$popup = new Popup();$popup->save(array('Name' => $name,'Content' => $content,'CreatedAt' => date('Y-m-d H:i:s'),'LastUpdate' => date('Y-m-d H:i:s')));$response['Name'] = $name;$response['Content'] = $content;$response['Id'] = $popup->getId();}return $response;}} |
And we come to the unit test, it’s worth noting that for reasons that are unknown to me, phpunit fails with a weird error when running the test with a DataProvider. The only way to get it to pass was by forcing the test to run in a separate process using the @runInSeparateProcess annotation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | tests/Tests/Silverbadge/Api/FacadeTest.php<?php/*** FacadeTest.php** @author rami*/namespace Tests\Silverbadge\Api;use Silverbadge\Api\Facade;use Phalcon\Mvc\Model\Manager;use Phalcon\Mvc\Model\Metadata\Files;use Phalcon\DI;class FacadeTest extends \PHPUnit_Framework_TestCase{/*** @dataProvider provideDataForTestCreateApplication* @runInSeparateProcess*/public function testCreateApplication($numRowsFound, $fetchAllData){$di = New DI();$di->set('modelsManager', new Manager());$di->set('modelsMetadata', new Files(array('metaDataDir' => __DIR__ . '/../../../resources/')));$con = $this->getMock('\\Phalcon\\Db\\Adapter\\Pdo\\Mysql', array('getDialect', 'query', 'execute'), array(),'',false);$dialect = $this->getMock('\\Phalcon\\Db\\Dialect\\Mysql', array('select'), array(), '', false);$results = $this->getMock('\\Phalcon\\Db\\Result\\Pdo', array('numRows', 'setFetchMode', 'fetchall'), array(), '', false);$results->expects($this->any())->method('numRows')->will($this->returnValue($numRowsFound));$results->expects($this->any())->method('fetchall')->will($this->returnValue($fetchAllData));$dialect->expects($this->any())->method('select');$con->expects($this->any())->method('getDialect')->will($this->returnValue($dialect));$con->expects($this->any())->method('query')->will($this->returnValue($results));$con->expects($this->any())->method('execute');$di->set('db', $con);$facade = new Facade();$facade->setDI($di);$actual = $facade->createNewPopup('Test Popup','This is a test');$this->assertTrue(is_array($actual));}public function provideDataForTestCreateApplication(){return array(array(0, null),array(1, array(array())),);}} |
So,there you go, 100% code coverage without mocking away methods that wraps the static model find methods or turning a blind eye on unit testing just because of Models.
Conclusion
Phalcon is a very flexible framework and can be unit tested with a bit of researching and code tracing, we can not only make our code green on our build boxes but also more meaningful and readable.