PHPUnit and its Database Extension. Quick look

    Extensive and verbose introduction


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

    Some tests are not used due to the fact that the tested functionality did not go into production and donkey for years and years in files and directories, and some just tired everyone of the chaos that was created there. All that entropy we produced was the result of our incompetence in unit testing and a non-systematic approach to their implementation. In our tests, almost all the principles of unit testing are violated. Starting from the fact that they depend on one another, ending with the fact that you can see test methods swollen with the preparatory work that are full of sql-syntax and much more ... Plus to all of the above, almost all previous attempts to introduce unit testing were mostly amateur authorized by the customer



    Smooth transition to the practical side of the issue


    This is all the lyrics. Now it’s more about 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 (but in reality 4 years).

    The time has come when even the most stubborn and “technically competent” customers have come to understand the need for the quality of code on which sympathetic PMs insist. The “pointer” came from above to do the real testing of unit testing. Even the tasks in the fat put appropriate.

    So, based on our experience of previous failures, we identified a number of problems:
    1. The problem of fixtures. Not everything that is taken from the base can be replaced by
      mokami. This is due to some unsystematic work with the database. We do not have ORM, and all the functions of the latter between the cases are performed by the model.
    2. Test run speed. The last attempt at implementation was drowned
      partly because of this, and partly ...
    3. Supportability. ... because of the unsupportability, the cause of which
      was the problem of item 1, solving that we equipped our tests with
      multi-line inserts and updates.

    We turned to a competent colleague in this matter - a Java developer. He then told us about such tools as JUnit and DbUnit. He 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 problem of the speed of tests, increasing the likelihood of their more frequent running, before they commit, they run the same tests on real base and then still finish the bugs continious integration system.

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

    Practical example


    Suppose we have a person table
    1. CREATETABLE person (
    2.      id INTEGER PRIMARYKEY AUTOINCREMENT,
    3.      name VARCHAR(255),
    4.      surname VARCHAR(255)
    5. );


    In order to test something in it, it is necessary that it be filled with some data. In the Database Extension PHPUnit, 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 must match the field names of the table.

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



    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.     publicfunction __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.     }
    17.     protected function getConnection()
    18.     {
    19.         return $this->createDefaultDBConnection($this->connection, 'sqlite');
    20.     }
    21.  
    22.     protected function getDataSet()
    23.     {
    24.         return $this->createFlatXMLDataSet(dirname(__FILE__).'/persons.xml');
    25.     }
    26. }


    Obviously - the test does not test anything, but we already did something there:
    It is inherited from PHPUnit_Extensions_Database_TestCase in which some methods are already implemented, including setUp (cleans the database and fills it again) and tearDown - which does the same thing. The PHPUnit dock details how these methods interact with test methods. In the constructor, for simplicity, I create the above plate and in the inmemory database. After I redefine the two abstract Database_TestCase methods. The first returns an object to connect to the database, and the second object created from our xml fixture. These methods are used in the database_TestCase setUp and tearDown methods defined.

    Now check by adding a new method
    1. publicfunction 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[00]['name']);
    9. }


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

    In a specific case, I needed to add some data during the execution of a specific test method, without finding anything in the  dock, I dig deeper into the code and came to the following solution:

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


    And we write such a test method:
    1. publicfunction 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[00]['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 question with Operations in the  official dock has not yet been described) an insert object that implements the PHPUnit_Extensions_Database_Operation_IDatabaseOperation interface which has only one method with the following signature:

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


    We then call it in the 4th line of the previous listing, and it, in turn, supplements 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 everything in life is not as complicated as in this example. Therefore, I ask you not to succumb to the artificiality of the criticism described.
    As the situation develops, I would like to devote you to all admissible details as an opportunity to remember everything excavated and not described in the documentation.
    Of course, only on the condition that someone needs this (I will soon understand this).

    Also popular now: