📜 ⬆️ ⬇️

PHPunit implementation practice

Enough has been said about the benefits of automated testing (for example, here and here ), but so far many people still do not write tests. One of the reasons, it seems to me, is that the proposed methods for automating testing are more complicated than necessary for most cases. Today I want to talk about how this is done with us.



The article is purely practical in nature and is designed for what you have an idea about phpunit. ZF, mysql + innodb are used for implementation, but you can use it with any tool if you wish. Additionally, dklab.ru/lib/PHP_Exceptionizer is used (converts notices and varnings into exceptions).
')

Training



The project folder creates the tests folder with the following structure.

d-application
d-models
d- triggers
...
run.php

The contents of the application and models folders follow the structure of the controllers and models in the project. We do not use test suites, so by adding a new test file to the system we do not need to add this test to the kit. In the triggers folder there are folders by the names of the tables, inside which are the files named as triggers.
The run.php file contains the code needed to perform the tests: autoloading of classes, connection to the database, etc. The script should work in the environment and in the battle. This can be done using Zend_Console_Getopt. At the end of the file line

PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT' );
PHPUnit_TextUI_Command::main();


* This source code was highlighted with Source Code Highlighter .


In order for them to work, phpunit must be installed on the system and configured to autoload.

To run the tests (s) from the tests folder, you can do this:
php run.php models / Article (all tests from the Article folder will start)
php run.php models / ArticleTest.php (all tests from the ArticleTest.php file will run)

Testing models



All tests are inherited from the Ext_Db_Table_Test_Abstract class, which contains a connection to the database. The setUp and TearDown methods are declared final, and they are replaced by _start and _finish. In setUp, the transaction starts, and in tearDown it is rolled back. Thus, each test is performed in its transaction, which saves us from having to monitor objects and clean the database.

<?
  1. <? php
  2. abstract class Ext_Db_Table_Test_Abstract extends PHPUnit_Framework_TestCase
  3. {
  4. / **
  5. * @var Zend_Db_Adapter_Abstract
  6. * /
  7. protected static $ _db;
  8. public static function setDbAdapter (Zend_Db_Adapter_Abstract $ db = null )
  9. {
  10. if (empty ($ db)) {
  11. $ db = Zend_Db_Table_Abstract :: getDefaultAdapter ();
  12. }
  13. self :: $ _ db = $ db;
  14. }
  15. / **
  16. * Zend_Db_Profiler
  17. *
  18. * @return Zend_Db_Profiler
  19. * /
  20. public function getProfiler ()
  21. {
  22. return self :: $ _ db-> getProfiler ();
  23. }
  24. final public function setUp ()
  25. {
  26. if (empty (self :: $ _ db)) {
  27. self :: setDbAdapter ();
  28. }
  29. self :: $ _ db-> beginTransaction (); // Each test in its transaction!
  30. $ this -> _ start ();
  31. }
  32. final public function tearDown ()
  33. {
  34. $ this -> _ finish ();
  35. self :: $ _ db-> rollBack ();
  36. }
  37. protected function _start ()
  38. {
  39. }
  40. protected function _finish ()
  41. {
  42. }
  43. }
* This source code was highlighted with Source Code Highlighter .


The next important point is the creation of fixtures. All testing articles suggest using the PHPUnit extension and its Database Extension for this. In general, this is correct, but we wanted it easier.

To create test data, there is a special class Test_Object, represented as singletone.
If we need an article we write like this $ article = Test_Object :: getInstance () -> addArticle ();

  1. public function addArticle (array $ data = array ())
  2. {
  3. $ base = array (
  4. 'key' => md5 (mt_rand ()),
  5. 'content' => md5 (mt_rand ()),
  6. 'name' => md5 (mt_rand ()),
  7. 'published' => 1,
  8. 'file_id' => $ this -> addFile () -> file_id, // Create a dependent object
  9. 'created' => now ()
  10. );
  11. $ article_table = Article :: getInstance ();
  12. if (empty ($ data [ 'article_category_id' ])) { // If the category id is not passed, the category is automatically created
  13. $ base [ 'article_category_id' ] = $ this -> addArticleCategory ($ data) -> article_category_id; // The $ data array is passed to the method that creates the category.
  14. }
  15. return $ this -> _ createRow ($ article_table, $ base , $ data);
  16. }
  17. protected function _createRow (Ext_Db_Table_Abstract $ table, array $ base , array $ data = array ())
  18. {
  19. $ data = array_merge ($ base , $ data);
  20. $ row = $ table-> createRow ($ data);
  21. $ row-> save ();
  22. return $ row;
  23. }
* This source code was highlighted with Source Code Highlighter .


Here you can see that we can easily override the data that we need by passing them to the addArticle. The same method creates objects inside of itself on which it depends (if you have not transferred them to it in $ data). Currently, Test_Object contains about 4000 lines of code due to the large number of entities created in the system. Imagine what we would use xml files. Another advantage of this approach is that the creation of objects is concentrated in one place and makes it easy to create any graph of objects. For example, we need a comment on the article; for this, the addArticleComment () method is written, which internally creates all the necessary objects addUser, AddArticle, etc. and returns the comment we need.

And now the test itself.

  1. <? php
  2. class ArticleTest extends Ext_Db_Table_Test_Abstract
  3. {
  4. public function testFindByKey ()
  5. {
  6. $ article = Test_Object :: getInstance () -> addArticle ();
  7. $ row = Article :: getInstance () -> findByKey ($ article-> key);
  8. $ this -> assertEquals ($ article, $ row);
  9. }
  10. }
* This source code was highlighted with Source Code Highlighter .


Running and checking php run.php models / Article / ArticleTest.php

Below is an example of running all model tests. There are about 1200 tests and about 4000 checks in our system (the total coverage is more than 90%).

php run.php model/
PHPUnit 3.3.12 by Sebastian Bergmann.

E........................................................... 60 / 356
............................................................ 120 / 356
............................................................ 180 / 356
F..........................................................I 240 / 356
............................................................ 300 / 356
........................................................

Time: 42 seconds
FAILURES!
Tests: 356, Assertions: 800, Failures: 1, Errors: 1, Incomplete: 1.


Controller testing



Easier to understand by example.
  1. <? php
  2. class Frontend_Tender_EditControllerTest extends ControllerTestCase // Class in which the acl environment, routing, etc. is raised
  3. {
  4. public function testAddAction () // Verify that the page is opened.
  5. {
  6. $ this -> dispatch ( '/ tender / add' );
  7. $ this -> assertNoErrors (); // Verifies that the plugin error did not register any errors.
  8. $ this -> assertModuleFromParams ( 'tender' );
  9. $ this -> assertControllerFromParams ( 'edit' );
  10. $ this -> assertActionFromParams ( 'add' );
  11. }
  12. public function testAddActionWithPost () // Check the form
  13. {
  14. $ data = array (
  15. 'name' => 'name' ,
  16. 'content' => md5 (mt_rand ()),
  17. 'phone' => '12345' ,
  18. 'country_id' => 3159,
  19. 'region_id' => 4312,
  20. 'city_id' => 4400,
  21. 'email' => md5 (mt_rand ()). '@ testemail.ru' ,
  22. );
  23. $ this -> getRequest () -> setMethod ( 'post' )
  24. -> setParams ($ data);
  25. $ this -> dispatch ( '/ tender / add' );
  26. $ this -> assertNoErrors ();
  27. $ table = Tender :: getInstance ();
  28. $ row = $ table-> selectByEmail ($ data [ 'email' ]) -> fetchRow (); // I will tell about it in the next topic)
  29. foreach ($ data as $ key => $ value) {
  30. $ this -> assertEquals ($ value, $ row -> $ key); // Check that our data is in the database
  31. }
  32. }
  33. }
* This source code was highlighted with Source Code Highlighter .


Methods assertModuleFromParams, assertControllerFromParams, assertActionFromParams check $ request-> getParam (blabla).
We actively use actionStack, and he changes the request all the time, so it is not advisable to check $ request-> getModuleName ().

Conclusion



The method presented here is just one of the possible. For us, it seemed very convenient and, we can say that it was tested by time. And if you haven't written tests yet, it's time to start doing this).

ps The first post on Habré. If it is interesting I will write about the architecture of the project in which I participate.

pss This scheme has been tested and used for www.okinfo.ru

Source: https://habr.com/ru/post/72716/


All Articles