开发者

PHPUnit - Asserting that a query returns the good data from a database

开发者 https://www.devze.com 2023-02-19 13:05 出处:网络
I\'m testing a Factory that simply retrieves all the \"post\" of a news system. I\'ll cut the example to something as simple as possible:

I'm testing a Factory that simply retrieves all the "post" of a news system. I'll cut the example to something as simple as possible:

$newsFactory->getAllNews();

The table looks like this:

+---------+---------------------+-------------+
| news_id | news_publishedDate  | news_active |
+---------+---------------------+-------------+
|       1 | 2010-03-22 13:20:22 |           1 |
|       2 | 2010-03-23 13:20:22 |           1 |
|      14 | 2010-03-23 13:20:22 |           0 |
|      15 | 2010-03-23 13:20:22 |           1 |
+---------+---------------------+-------------+

I want to test that behaviour; for now, we'll focus only on the first one:

  • Make sure the query will return only news_active=1
  • Make sure the query will return the element ordered by news_publishedDate, from newest to older.

So I've made an dbData.xml dataset of what I consider as good testing data:

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
  <table name="news">
    <column>news_id</column>
    <column>news_publishedDate</column>
    <column>news_active</column>
    <row>
        <value>1</value>
        <value>2010-03-20 08:55:05</value>
        <value>1</value>
    </row>
    <row>
        <value>2</value>
        <value>2010-03-20 08:55:05</value>
        <value>0</value>
    </row>
    <row>
        <value>3</value>
        <value>2011-03-20 08:55:05</value>
        <value>1</value>
    </row>
  </table>
</dataset>

Ok, so let's just check the first test (not returning the news_id #2 from the XML data set)

I must extend the PHPUnit_Extensions_Database_TestCase class to make my NewsFactoryTest class:

<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';

class NewsFactoryTest extends PHPUnit_Extensions_Database_TestCase
{
    protected $db;


    protected function getConnection()
    {
        $this->db = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
        return $this->createDefaultDBConnection($this->db, 'testdb');
    }

    protected function getDataSet()
    {
        return $this->createXMLDataSet(dir(__FILE__) . DIRECTORY_SEPARATOR . 'dbData.xml');
    }

    public function testGetNewsById()
    {
        $newsFactory = new NewsFactory($this->db);
        $news = $newsFactory->getNewsById();
        // ???
        $this->assertEquals(2, count($news), "Should return only 2 results");
    }
}

My main question would be how do I setup that test ?

In details, I try to understand:

  • Should I create a testdb database or is that all emulated/virtual ?
    • I've seen many examples using sqlite::memory:, is it a good idea to test MySQL based query with sqlite? Can I use mysql::memory: instead ?
    • If it's a real DB, how do I restore all the data from dbData.x开发者_如何学JAVAml in the DB before each test run ?
  • Where am I supposed to call getConnection() and getDataSet()?

Thanks for reading & sharing your knowledge!


I setup database testing in our project and here are some answers and lessons that worked for us:

Should I create a testdb database or is that all emulated/virtual ?

I was asking the same question at the beginning and we both learned that it is indeed, a real database running.

I've seen many examples using sqlite::memory: , is it a good idea to test MySQL based query with sqlite ? Can I use mysql::memory: instead ?

I tried to use sqlite for performance, but found that the SQL would be different enough not to be usable every where with our existing code. I was able to use the MySQL MEMORY engine for most tables thought (not possible for some tables such as BLOB columns).

If it's a real DB, how do I restore all the data from dbData.xml in the DB before each test run ?

I wrote a script to call mysqldump of the schemas and all their tables from our remote test server, insert them in the local server, and convert all possible table engines to MEMORY. This does take time, but as the schemas don't change between tests, it is only run once at the top most TestSuite or separately as a developer needs on their local system.

The datasets are loaded at the beginning of each test and since the table already exists and is in memory, the inserting and truncating between tests is fast.

Where am I supposed to call getConnection() and getDataSet() ?

We already had a helper class that extended TestCase, so I couldn't use PHPUnit_Extensions_Database_TestCase. I add setup functions into that helper class and never called or had to implement getDataSet(). I did use getConnection() to create datasets from modified data in an assert function.

/**
 * @param PHPUnit_Extensions_Database_DataSet_IDataSet $expected_data_fixture
 * @param string|array $tables
 */
protected function assertDataFixturesEqual($expected_data_fixture, $tables){
    if(!is_array($tables)){
        $tables = array($tables);
    }
    PHPUnit_Extensions_Database_TestCase::assertDataSetsEqual($expected_data_fixture, $this->DbTester->getConnection()->createDataSet($tables));
}

EDIT: I found some bookmarks of resources I used as the PHPUnit documentation is a little lacking:

http://www.ds-o.com/archives/63-PHPUnit-Database-Extension-DBUnit-Port.html

http://www.ds-o.com/archives/64-Adding-Database-Tests-to-Existing-PHPUnit-Test-Cases.html


I haven't used PHPUnit's database test case so I must confine my answer to the assertion. You can either assert that ID 2 is not present in $news or you can assert that every object in $news is inactive. The latter is more flexible as you won't need to change your test as you add data to the test dataset.

$news = $newsFactory->getNewsById();
foreach ($news as $item) {
    self::assertTrue($news->isActive());
}

BTW, the published dates in your dataset are all identical. This will make testing the ordering impossible. ;)


So far I've understood that: Should I create a testdb database or is that all emulated/virtual ?

It create a real database, and using the getSetUpOperation method, it's really slow as the tables are truncated and re-imported for each test, and it's demanding alot on the harddrive even for a small amount of data. ( ~ 1 sec/test )

I've seen many examples using sqlite::memory: , is it a good idea to test MySQL based query with sqlite ? Can I use mysql::memory: instead ?

I still don't know. I think it's now really possible with MySQL.

If it's a real DB, how do I restore all the data from dbData.xml in the DB before each test run ?

There are getSetUpOperation and getTearDownOperation that act like setup and tearDown method. Adding this will truncate the table mentionned in the dataSet, and re-insert all the data of that xml file:

/**
 * Executed before each
 *
 * @return PHPUnit_Extensions_Database_Operation_DatabaseOperation
 */
protected function getSetUpOperation()
{
    return PHPUnit_Extensions_Database_Operation_Factory::CLEAN_INSERT();
}

Where am I supposed to call getConnection() and getDataSet() ?

Nowhere. Theses are magic method that are called automatically. getConnection is called before the tests ( a bit like __construct would be, but I'm not sure about the order ) and getDataSet will be called when a dataSet is needed. I think that in my case, only getSetUpOperation have a dependency for a dataSet... so in the background it calls the getDataSet method before each tests to make the CLEAN_INSERT operation.

Also, I discovered that we need to create the table structure ( the dataset doesn't handle that ), so my full --slow-- working code is:

<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';

class NewsFactoryTest extends PHPUnit_Extensions_Database_TestCase
{
/**
 * Custom PDO instance required by the SUT.
 *
 * @var Core_Db_Driver_iConnector
 */
protected $db;

/**
 * Create a connexion.
 * Note: les constantes de connexion sont définit dans bootstrap.php.
 * 
 * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
 */
protected function getConnection()
{

    //Instanciate the connexion required by the system under test.
    $this->db = new Core_Db_Driver_PDO('mysql:host=' . TEST_DB_HOST . ';dbname=' . TEST_DB_BASE, TEST_DB_USER, TEST_DB_PASS, array());

    //Create a genuine PDO connexion, required for PHPUnit_Extensions_Database_TestCase.
    $db = new PDO('mysql:host=' . TEST_DB_HOST . ';dbname=' . TEST_DB_BASE, TEST_DB_USER, TEST_DB_PASS);
    $this->createTableSchema($db);
    return $this->createDefaultDBConnection($db, TEST_DB_BASE);
}

/**
 * Load the required table schemes.
 *
 * @param PDO $db
 * @return void
 */
protected function createTableSchema(PDO $db)
{
    $schemaPath = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'sql_schema' . DIRECTORY_SEPARATOR;
    $query = file_get_contents($schemaPath . 'news.sql');

    $db->exec($query);

    $query = file_get_contents($schemaPath . 'news_locale.sql');
    $db->exec($query);
}

/**
 * Load the dataSet in memory.
 *
 * @return PHPUnit_Extensions_Database_DataSet_IDataSet
 */
protected function getDataSet()
{
    return $this->createXMLDataSet(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'newsFactory_dataSet.xml');
}

/**
 * Method executed before each test
 *
 * @return PHPUnit_Extensions_Database_Operation_DatabaseOperation
 */
protected function getSetUpOperation()
{
    //TRUNCATE the table mentionned in the dataSet, then re-insert the content of the dataset.
    return PHPUnit_Extensions_Database_Operation_Factory::CLEAN_INSERT();
}

/**
 * Method executed after each test
 *
 * @return PHPUnit_Extensions_Database_Operation_DatabaseOperation
 */
protected function getTearDownOperation()
{
    //Do nothing ( yup, their's a code for that )
    return PHPUnit_Extensions_Database_Operation_Factory::NONE();
}


/**
 * @covers NewsFactory::getNewsById
 */
public function testGetNewsById()
{
    $newsFactory = new NewsFactory($this->db);
    $news = $newsFactory->getNewsById(999);
    $this->assertFalse($news);
}

}

Hope that will help other people that needed some extra explanations. If you have any comment, suggestion or idea, your input is welcome, as I don't consider this solution as a fully efficient one. ( Slow and long to setup, and needs a double connection. )

0

精彩评论

暂无评论...
验证码 换一张
取 消