Prev Next

Chapter 9. Database Testing

While creating tests for your software you may come across database code that needs to be unit tested. The database extension has been created to provide an easy way to place your database in a known state, execute your database-effecting code, and ensure that the expected data is found in the database.

The quickest way to create a new Database Unit Test is to extend the PHPUnit_Extensions_Database_TestCase class. This class provides the functionality to create a database connection, seed your database with data, and after executing a test comparing the contents of your database with a data set that can be built in a variety of formats. In Example 9.1 you can see examples of getConnection() and getDataSet() implementations.

Example 9.1: Setting up a database test case

<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';
 
class DatabaseTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getConnection()
    {
        $pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
        return $this->createDefaultDBConnection($pdo, 'testdb');
    }
 
    protected function getDataSet()
    {
        return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/bank-account-seed.xml');
    }
}
?>


The getConnection() method must return an implementation of the PHPUnit_Extensions_Database_DB_IDatabaseConnection interface. The createDefaultDBConnection() method can be used to return a database connection. It accepts a PDO object as the first parameter and the name of the schema you are testing against as the second parameter.

The getDataSet() method must return an implementation of the PHPUnit_Extensions_Database_DataSet_IDataSet interface. There are currently three different data sets available in PHPUnit. These data sets are discussed in the section called “Data Sets”

Table 9.1. Database Test Case Methods

MethodMeaning
PHPUnit_Extensions_Database_DB_IDatabaseConnection getConnection()Implement to return the database connection that will be checked for expected data sets and tables.
PHPUnit_Extensions_Database_DataSet_IDataSet getDataSet()Implement to return the data set that will be used in in database set up and tear down operations.
PHPUnit_Extensions_Database_Operation_DatabaseOperation getSetUpOperation()Override to return a specific operation that should be performed on the test database at the beginning of each test. The various operations are detailed in the section called “Operations”.
PHPUnit_Extensions_Database_Operation_DatabaseOperation getTearDownOperation()Override to return a specific operation that should be performed on the test database at the end of each test. The various operations are detailed in the section called “Operations”.
PHPUnit_Extensions_Database_DB_DefaultDatabaseConnection createDefaultDBConnection(PDO $connection, string $schema)Return a database connection wrapper around the $connection PDO object. The database schema being tested against should be specified by $schema. The result of this method can be returned from getConnection().
PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet createFlatXMLDataSet(string $xmlFile)Returns a flat XML data set that is created from the XML file located at the absolute path specified in $xmlFile. More details about flat XML files can be found in the section called “Flat XML Data Set”. The result of this method can be returned from getDataSet().
PHPUnit_Extensions_Database_DataSet_XmlDataSet createXMLDataSet(string $xmlFile)Returns a XML data set that is created from the XML file located at the absolute path specified in $xmlFile. More details about XML files can be found in the section called “XML Data Set”. The result of this method can be returned from getDataSet().
void assertTablesEqual(PHPUnit_Extensions_Database_DataSet_ITable $expected, PHPUnit_Extensions_Database_DataSet_ITable $actual)Reports an error if the contents of the $expected table do not match the contents in the $actual table.
void assertDataSetsEqual(PHPUnit_Extensions_Database_DataSet_IDataSet $expected, PHPUnit_Extensions_Database_DataSet_IDataSet $actual)Reports an error if the contents of the $expected data set do not match the contents in the $actual data set.


Data Sets

Data sets are the basic building blocks for both your database fixture as well as the assertions you may make at the end of your test. When returning a data set as a fixture from the PHPUnit_Extensions_Database_TestCase::getDataSet() method, the default implementation in PHPUnit will automatically truncate all tables specified and then insert the data from your data set in the order specified by the data set. For your convenience there are several different types of data sets that can be used at your convenience.

Flat XML Data Set

The flat XML data set is a very simple XML format that uses a single XML element for each row in your data set. An example of a flat XML data set is shown in Example 9.2.

Example 9.2: A Flat XML Data Set

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
  <post 
    post_id="1" 
    title="My First Post" 
    date_created="2008-12-01 12:30:29" 
    contents="This is my first post" rating="5"
  />
  <post 
    post_id="2" 
    title="My Second Post" 
    date_created="2008-12-04 15:35:25" 
    contents="This is my second post" 
  />
  <post 
    post_id="3" 
    title="My Third Post" 
    date_created="2008-12-09 03:17:05" 
    contents="This is my third post" 
    rating="3" 
  />

  <post_comment 
    post_comment_id="2" 
    post_id="2" 
    author="Frank" 
    content="That is because this is simply an example." 
    url="http://phpun.it/" 
  />
  <post_comment 
    post_comment_id="1" 
    post_id="2" 
    author="Tom" 
    content="While the topic seemed interesting the content was lacking." 
  />

  <current_visitors />
</dataset>


As you can see the formatting is extremely simple. Each of the elements within the root <dataset> element represents a row in the test database with the exception of the last <current_visitors /> element (this will be discussed shortly.) The name of the element is the equivalent of a table name in your database. The name of each attribute is the equivalent of a column name in your databases. The value of each attribute is the equivalent of the value of that column in that row.

This format, while simple, does have some special considerations. The first of these is how you deal with empty tables. With the default operation of CLEAN_INSERT you can specify that you want to truncate a table and not insert any values by listing that table as an element without any attributes. This can be seen in Example 9.2 with the <current_visitors /> element. The most common reason you would want to ensure an empty table as a part of your fixture is when your test should be inserting data to that table. The less data your database has, the quicker your tests will run. So if I where testing my simple blogging software's ability to add comments to a post, I would likely change Example 9.2 to specify post_comment as an empty table. When dealing with assertions it is often useful to ensure that a table is not being unexpectedly written to, which could also be accomplished in FlatXML using the empty table format.

The second consideration is how NULL values are defined. The nature of the flat XML format only allows you to explicitly specify strings for column values. Of course your database will convert a string representation of a number or date into the appropriate data type. However, there are no string representations of NULL. You can imply a NULL value by leaving a column out of your element's attribute list. This will cause a NULL value to be inserted into the database for that column. This leads me right into my next consideration that makes implicit NULL values somewhat difficult to deal with.

The third consideration is how columns are defined. The column list for a given table is determined by the attributes in the first element for any given table. In Example 9.2 the post table would be considered to have the columns post_id, title, date_created, contents and rating. If the first <post> were removed then the post would no longer be considered to have the rating column. This means that the first element of a given name defines the structure of that table. In the simplest of examples, this means that your first defined row must have a value for every column that you expect to have values for in the rest of rows for that table. If an element further into your data set specifies a column that was not specified in the first element then that value will be ignored. You can see how this influenced the order of elements in my dataset in Example 9.2. I had to specify the second <post_comment> element first due to the non-NULL value in the url column.

There is a way to work around the inability to explicitly set NULL in a flat XML data using the Replacement data set type. This will be discussed further in the section called “Replacement Data Set”.

XML Data Set

While the flat XML data set is simple it also proves to be limiting. A more powerful xml alternative is the XML data set. It is a more structured xml format that allows you to be much more explicit with your data set. An example of the XML data set equivalent to the previous flat XML example is shown in Example 9.3.

Example 9.3: A XML Data Set

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
  <table name="post">
    <column>post_id</column>
    <column>title</column>
    <column>date_created</column>
    <column>contents</column>
    <column>rating</column>
    <row>
      <value>1</value>
      <value>My First Post</value>
      <value>2008-12-01 12:30:29</value>
      <value>This is my first post</value>
      <value>5</value>
    </row>
    <row>
      <value>2</value>
      <value>My Second Post</value>
      <value>2008-12-04 15:35:25</value>
      <value>This is my second post</value>
      <null />
    </row>
    <row>
      <value>3</value>
      <value>My Third Post</value>
      <value>2008-12-09 03:17:05</value>
      <value>This is my third post</value>
      <value>3</value>
    </row>
  </table>
  <table name="post_comment">
    <column>post_comment_id</column>
    <column>post_id</column>
    <column>author</column>
    <column>content</column>
    <column>url</column>
    <row>
      <value>1</value>
      <value>2</value>
      <value>Tom</value>
      <value>While the topic seemed interesting the content was lacking.</value>
      <null />
    </row>
    <row>
      <value>2</value>
      <value>2</value>
      <value>Frank</value>
      <value>That is because this is simply an example.</value>
      <value>http://phpun.it</value>
    </row>
  </table>
  <table name="current_visitors">
    <column>current_visitors_id</column>
    <column>ip</column>
  </table>
</dataset>


The formatting is more verbose than that of the Flat XML data set but in some ways much easier to understand. The root element is again the <dataset> element. Then directly under that element you will have one or more <table> elements. Each <table> element must have a name attribute with a value equivalent to the name of the table being represented. The <table> element will then include one or more <column> elements containing the name of a column in that table. The <table> element will also include any number (including zero) of <row> elements. The <row> element will be what ultimately specifies the data to store/remove/update in the database or to check the current database against. The <row> element must have the same number of children as there are <column> elements in that <table> element. The order of your child elements is also determined by the order of your <column> elements for that table. There are two different elements that can be used as children of the <row> element. You may use the <value> element to specify the value of that column in much the same way you can use attributes in the flat XML data set. The content of the <value> element will be considered the content of that column for that row. You may also use the <null> element to explicitly indicate that the column is to be given a NULL value. The <null> element does not contain any attributes or children.

You can use the DTD in Example 9.4 to validate your XML data set files. A reference of the valid elements in an XML data set can be found in Table 9.2

Example 9.4: The XML Data Set DTD

<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT dataset (table+) | ANY>
<!ELEMENT table (column*, row*)>
<!ATTLIST table
    name CDATA #REQUIRED
>
<!ELEMENT column (#PCDATA)>
<!ELEMENT row (value | null | none)*>
<!ELEMENT value (#PCDATA)>
<!ELEMENT null EMPTY>


Table 9.2. XML Data Set Element Description

ElementPurposeContentsAttributes
<dataset>The root element of the xml data set file.One or more <table> elements.None
<table>Defines a table in the dataset.One or more <column> elements and zero or more <row> elements.name - The name of the table.
<column>Defines a column in the current table.A text node containing the name of the column.None
<row>Defines a row in the table.One or more <value> or <null> elements.None
<value>Sets the value of the column in the same position as this value.A text node containing the value of the corresponding column.None
<null>Sets the value of the column in the same position of this value to NULL.NoneNone


CSV Data Set

While XML data sets provide a convenient way to structure your data, in many cases they can be time consuming to hand edit as there are not very many XML editors that provide an easy way to edit tabular data via xml. In these cases you may find the CSV data set to be much more useful. The CSV data set is very simple to understand. Each CSV file represents a table. The first row in the CSV file must contain the column names and all subsequent rows will contain the data for those columns.

To construct a CSV data set you must instantiate the PHPUnit_Extensions_Database_DataSet_CsvDataSet class. The constructor takes three parameters: $delimiter, $enclosure and $escape. These parameters allow you to specify the exact formatting of rows within your CSV file. So this of course means it doesn't have to really be a CSV file. You can also provide a tab delimited file. The default constructor will specify a comma delimited file with fields enclosed by a double quote. If there is a double quote within a value it will be escaped by an additional double quote. This is as close to an accepted standard for CSV as there is.

Once your CSV data set class is instantiated you can use the method addTable() to add CSV files as tables to your data set. The addTable method takes two parameters. The first is $tableName and contains the name of the table you are adding. The second is $csvFile and contains the path to the CSV you will be using to set the data for that table. You can call addTable() for each table you would like to add to your data set.

In Example 9.5 you can see an example of how three CSV files can be combined into the database fixture for a database test case.

Example 9.5: CSV Data Set Example

 
--- fixture/post.csv ---
post_id,title,date_created,contents,rating
1,My First Post,2008-12-01 12:30:29,This is my first post,5
2,My Second Post,2008-12-04 15:35:25,This is my second post,
3,My Third Post,2008-12-09 03:17:05,This is my third post,3
 
--- fixture/post_comment.csv ---
post_comment_id,post_id,author,content,url
1,2,Tom,While the topic seemed interesting the content was lacking.,
2,2,Frank,That is because this is simply an example.,http://phpun.it
 
--- fixture/current_visitors.csv ---
current_visitors_id,ip
 
--- DatabaseTest.php ---
<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';
require_once 'PHPUnit/Extensions/Database/DataSet/CsvDataSet.php';
 
class DatabaseTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getConnection()
    {
        $pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
        return $this->createDefaultDBConnection($pdo, 'testdb');
    }
 
    protected function getDataSet()
    {
    $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
    $dataSet->addTable('post', 'fixture/post.csv');
    $dataSet->addTable('post_comment', 'fixture/post_comment.csv');
    $dataSet->addTable('current_visitors', 'fixture/current_visitors.csv');
        return $dataSet;
    }
}
?>


Unfortunately, while the CSV dataset is appealing from the aspect of edit-ability, it has the same problems with NULL values as the flat XML dataset. There is no native way to explicitly specify a null value. If you do not specify a value (as I have done with some of the fields above) then the value will actually be set to that data type's equivalent of an empty string. Which in most cases will not be what you want. I will address this shortcoming of both the CSV and the flat XML data sets shortly.

Replacement Data Set

...

Operations

...

Database Testing Best Practices

...

Prev Next
1. Automating Tests
2. PHPUnit's Goals
3. Installing PHPUnit
4. Writing Tests for PHPUnit
Test Dependencies
Data Providers
Testing Exceptions
Testing PHP Errors
5. The Command-Line Test Runner
6. Fixtures
More setUp() than tearDown()
Variations
Sharing Fixture
Global State
7. Organizing Tests
Composing a Test Suite Using the Filesystem
Composing a Test Suite Using XML Configuration
Using the TestSuite Class
8. TestCase Extensions
Testing Output
9. Database Testing
Data Sets
Flat XML Data Set
XML Data Set
CSV Data Set
Replacement Data Set
Operations
Database Testing Best Practices
10. Incomplete and Skipped Tests
Incomplete Tests
Skipping Tests
11. Test Doubles
Stubs
Mock Objects
Stubbing and Mocking Web Services
Mocking the Filesystem
12. Testing Practices
During Development
During Debugging
13. Test-Driven Development
BankAccount Example
14. Code Coverage Analysis
Specifying Covered Methods
Ignoring Code Blocks
Including and Excluding Files
15. Other Uses for Tests
Agile Documentation
Cross-Team Tests
16. Skeleton Generator
Generating a Test Case Class Skeleton
Generating a Class Skeleton from a Test Case Class
17. PHPUnit and Selenium
Selenium RC
PHPUnit_Extensions_SeleniumTestCase
18. Logging
Test Results (XML)
Test Results (TAP)
Test Results (JSON)
Code Coverage (XML)
19. Build Automation
Apache Ant
Apache Maven
Phing
20. Continuous Integration
Atlassian Bamboo
CruiseControl
phpUnderControl
21. PHPUnit API
Overview
PHPUnit_Framework_Assert
assertArrayHasKey()
assertClassHasAttribute()
assertClassHasStaticAttribute()
assertContains()
assertContainsOnly()
assertEmpty()
assertEqualXMLStructure()
assertEquals()
assertFalse()
assertFileEquals()
assertFileExists()
assertGreaterThan()
assertGreaterThanOrEqual()
assertInstanceOf()
assertInternalType()
assertLessThan()
assertLessThanOrEqual()
assertNull()
assertObjectHasAttribute()
assertRegExp()
assertStringMatchesFormat()
assertStringMatchesFormatFile()
assertSame()
assertSelectCount()
assertSelectEquals()
assertSelectRegExp()
assertStringEndsWith()
assertStringEqualsFile()
assertStringStartsWith()
assertTag()
assertThat()
assertTrue()
assertType()
assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString()
PHPUnit_Framework_Test
PHPUnit_Framework_TestCase
PHPUnit_Framework_TestSuite
PHPUnit_Framework_TestResult
Package Structure
22. Extending PHPUnit
Subclass PHPUnit_Framework_TestCase
Assert Classes
Subclass PHPUnit_Extensions_TestDecorator
Implement PHPUnit_Framework_Test
Subclass PHPUnit_Framework_TestResult
Implement PHPUnit_Framework_TestListener
New Test Runner
A. Assertions
B. Annotations
@assert
@author
@backupGlobals
@backupStaticAttributes
@covers
@dataProvider
@depends
@expectedException
@expectedExceptionCode
@expectedExceptionMessage
@group
@outputBuffering
@runTestsInSeparateProcesses
@runInSeparateProcess
@test
@testdox
@ticket
C. The XML Configuration File
PHPUnit
Test Suites
Groups
Including and Excluding Files for Code Coverage
Logging
Test Listeners
Setting PHP INI settings, Constants and Global Variables
Configuring Browsers for Selenium RC
D. Index
E. Bibliography
F. Copyright