📜 ⬆️ ⬇️

PHPUnit and its Database Extension. Quick look

Spacious and wordy introduction


Already a little over a year in a project where I work there is talk of unit testing. In addition to conversations, attempts have been made repeatedly to turn these conversations into life. All attempts at the moment ended with the fact that none of the previously written unit test is run during the development process. All of them are dead code in the depths of our system. Imagine such a post-Soviet industrial? Sticking columns from the ground, rusty reinforcement against the gloomy sky :)

Some tests are not used due to the fact that the tested functionality has not been released in production and for years the donkey has been in files and directories, and some have simply exhausted everyone with the chaos that was created there. All the entropy produced by us was the result of our incompetence in unit testing and a non-systematic approach to their implementation. In our tests violated almost all the principles of unit testing. Starting from the fact that they depend on one another and ending with the fact that they can see test methods swelling from preparatory work, dazzling with sql-syntax, and much more ... In addition to the above mentioned, almost all previous attempts to introduce unit testing were more amateur sanctioned by the customer, because as soon as the plug-in occurred, the tests were thrown and the functionality began to be written in isolation from the tests, and is it worth saying that no one was going to return to the completion of the tests.


')

Smooth transition to the practical side of the issue


This is all the lyrics. Now go more on the case. As you can understand the project is written in php, not small and has been living for at least more than a year (and in fact 4 years).

The time has come when even the most stubborn and “technically competent” customers began to understand the need for the quality of the code on which sympathizing PMs so insist. Above came a “pointer” to do unit testing in the present. Even the tasks in fat put appropriate.

So based on our experience of past failures, we identified a number of problems:
  1. The problem of fixtures. Not everything that comes from the database can be replaced.
    mokami. This is due to some unsystematic work with the database. We do not have an ORM and all the functions of the latter are performed by the model between times.
  2. Test run speed. The last attempt to implement choked
    partly because of this, and partly because of ...
  3. Maintainability ... due to unsupported reason
    became the problem of claim 1, which we have equipped our tests
    multiline inserts and updates.

We turned to a colleague competent in this matter - a Java developer. He told us about such tools as JUnit and DbUnit. He also told us about the process that they use with the participation of this toolkit: during development they run tests on in-memory databases, thus solving the task of test speeds increasing the likelihood of their more frequent launch, before commits they run the same tests on real base and then still finish off the bugs by continious integration system.

We used xUnit framework (SimpleTest) earlier, but for the first time we heard about DbUnit. Actually about him and will be discussed further. For php, there is a DbUnit implementation as an extension to PHPUnit. The meaning of its use is that all fixtures are prepared in the form of xml files and filled in to the test database before running each test method. This, in my opinion, solves the problem of maintaining fixtures, because xml is more readable than sql-dumps.

Practical example


Suppose we have a person table
  1. CREATE TABLE person ( id INTEGER PRIMARY KEY AUTOINCREMENT , name VARCHAR ( 255 ) , surname VARCHAR ( 255 ) ) ;
  2. CREATE TABLE person ( id INTEGER PRIMARY KEY AUTOINCREMENT , name VARCHAR ( 255 ) , surname VARCHAR ( 255 ) ) ;
  3. CREATE TABLE person ( id INTEGER PRIMARY KEY AUTOINCREMENT , name VARCHAR ( 255 ) , surname VARCHAR ( 255 ) ) ;
  4. CREATE TABLE person ( id INTEGER PRIMARY KEY AUTOINCREMENT , name VARCHAR ( 255 ) , surname VARCHAR ( 255 ) ) ;
  5. CREATE TABLE person ( id INTEGER PRIMARY KEY AUTOINCREMENT , name VARCHAR ( 255 ) , surname VARCHAR ( 255 ) ) ;


In order to test something in it, it is necessary that it be filled with some kind of data. In Database Extension PHPUnit, the content is described in xml format, one element for each row in the table. The element name must match the table name, and the attribute names the field names of the table.

Create such persons.xml with fixture for this table:
  1. <? xml version = "1.0" encoding = "UTF-8" ?>
  2. <dataset >
  3. <person
  4. id = "1"
  5. name = "Nalabal"
  6. surname = "Nadjava" />
  7. </ dataset >



Now attention test case:
  1. require_once 'PHPUnit / Extensions / Database / TestCase.php' ;
  2. require_once 'PHPUnit / Framework.php' ;
  3. class PersonTest extends PHPUnit_Extensions_Database_TestCase
  4. {
  5. public function __construct ( )
  6. {
  7. $ this -> connection = new PDO ( 'sqlite :: memory:' ) ;
  8. $ this -> connection -> query ( "
  9. CREATE TABLE person (
  10. id INTEGER PRIMARY KEY AUTOINCREMENT,
  11. name VARCHAR (255),
  12. surname VARCHAR (255)
  13. );
  14. " ) ;
  15. }
  16. protected function getConnection ( )
  17. {
  18. return $ this -> createDefaultDBConnection ( $ this -> connection , 'sqlite' ) ;
  19. }
  20. protected function getDataSet ( )
  21. {
  22. return $ this -> createFlatXMLDataSet ( dirname ( __FILE__ ) . '/persons.xml' ) ;
  23. }
  24. }


Obviously - the test does not test anything, but something we have already done there:
It inherits from PHPUnit_Extensions_Database_TestCase in which some methods are already implemented, including setUp (clears the base and fills it again) and tearDown - which does the same. In the PHPUnit dock, a more detailed description of how these methods interact with test methods. In the constructor, for simplicity, I create the above label and in the inmemory database. After I override the two abstract methods Database_TestCase. The first one returns the connection object with the base, and the second object created from our xml fixture. These methods are used in the methods defined in Database_TestCase setUp and tearDown.

Now check by adding a new method
  1. public function testPerson ( )
  2. {
  3. $ sql = "SELECT * FROM person" ;
  4. $ statement =
  5. $ this -> getConnection ( ) -> getConnection ( ) -> query ( $ sql ) ;
  6. $ result = $ statement -> fetchAll ( ) ;
  7. $ this -> assertEquals ( 1 , sizeof ( $ result ) ) ;
  8. $ this -> assertEquals ( 'Nalabal' , $ result [ 0 0 ] [ 'name' ] ) ;
  9. }


Result: OK (1 test, 2 assertions)
In the method, select all the records from the persons table and check the selection for compliance with our fixture

In a particular case, I needed to add some data during the execution of a specific test method, not finding anything in the dock, I searched the code a bit and came to the following solution:

Add new fixers additionalPersons.xml
  1. <? xml version = "1.0" encoding = "UTF-8" ?>
  2. <dataset >
  3. <person
  4. id = "2"
  5. name = "Nageneril"
  6. surname = "Mudrapragram" />
  7. <person
  8. id = "3"
  9. name = "Strudomprassal"
  10. surname = "Vashapragram" />
  11. </ dataset >


And we write this test method:
  1. public function testAdditionalPerson ( )
  2. {
  3. $ insertOperation = PHPUnit_Extensions_Database_Operation_Factory :: INSERT ( ) ;
  4. $ insertOperation -> execute ( $ this -> getConnection ( ) , $ this -> createFlatXMLDataSet ( dirname ( __FILE__ )) . '/additionalPersons.xml' ) ) ;
  5. $ sql = "SELECT * FROM person" ;
  6. $ statement = $ this -> getConnection ( ) -> getConnection ( ) -> query ( $ sql ) ;
  7. $ result = $ statement -> fetchAll ( ) ;
  8. $ this -> assertEquals ( 3 , sizeof ( $ result ) ) ;
  9. $ this -> assertEquals ( 'Nalabal' , $ result [ 0 0 ] [ 'name' ] ) ;
  10. $ this -> assertEquals ( 'Nageneril' , $ result [ 1 ] [ 'name' ] ) ;
  11. $ this -> assertEquals ( 'Strudomprassal' , $ result [ 2 ] [ 'name' ] ) ;
  12. }


In the 3rd line we create using Operation_Factory (the issue with Operations in the official dock is not yet described) an insertion object that implements the PHPUnit_Extensions_Database_Operation_IDatabaseOperation interface which has only one method with the following signature:

  1. public function execute ( PHPUnit_Extensions_Database_DB_IDatabaseConnection $ connection , PHPUnit_Extensions_Database_DataSet_IDataSet $ dataSet ) ;


This is what we call in the 4th line of the previous listing, and he in turn complements our initial fixture from persons.xml with data from additionalPersons.xml

The result of the launch will be: OK (2 tests, 6 assertions)

Conclusion


It is likely that in life everything is not as difficult as in this example. Therefore, I ask you not to be artificial to the criticism.
As the situation develops, I would like to dedicate you to all permissible details as an opportunity to remember everything that has been excavated and not described in the documentation.
Of course, only under the condition that someone needs it (I will soon understand).

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


All Articles