Database Integration Testing With Enhance PHP
<< September | October | November >>
Thursday, 13th October 2011
Before we even get started on the Enhance PHP bit - let's remind ourselves of where integration tests fit in to our testing strategy. We will do this by comparing them to unit tests.
When you write a unit test, your test is the client. It is the consumer of the code. If you do it right, it is the best documentation you could ever write for your code as it describes how to call your code and what you should expect it to do.
Below your unit test, you have your actual code. This is the stuff you want to test. You want to have as little real code involved in your test as possible, because real code might change and affect your tests.
Because you don't want your tests to rely on lots of other real code, you normally use mocks and stubs to "stand-in" for the real code. Your mocks and stubs are pre-programmed to give predictable answers to the questions your code will ask.
A typical example would be that you are testing some logic that relies on a database. You would make sure you could supply a fake data-access layer to your logic so you don't have to rely on a database being available that would have to contain data in a certain state. So you would supply a mock or stub in place of the real data access.
But how do you know that your data access will work when you use the system in real life? This is where integration tests step in to save the day. An integration test will check that your data-access code can interact with your database correctly, but how do you deal with the problem of needing a database in the correct state?
Some people try to get hold of a fake-implementation of their database engine to take the place of the real one in order to test their data-access code, but if this behaves in a different way to the real thing your tests are invalid.
When I write an integration test, I want to test that my data access works with my database engine - that is the test! But how do you deal with the problem of needing data in the correct state? You can try to keep a copy of a database that is in the correct state, but it is very hard to keep it in the right state and also keep the tables in step with any changes. It is also possible that if some of your tests fail, the database will be left in a mess, so you would need to restore a previous snapshot of the data. I think that there is a better way, so here is how I do integration tests for my database layer.
I start my integration tests with a totally blank database. The database is only ever used for running integration tests. I then perform the following for each test.
- Create the required table (sometimes more than one table is required)
- Set up a small amount of data to test against
- Run the test
- Drop the table
Because I start and end with an empty database, it doesn't matter what order I run the tests in and I can also run a single test, because it doesn't require any other tests to run before it can be performed.
Let's put that to practice with a simple example in PHP. First of all we write a new class to contain our tests.
class CompanyRepositoryTestFixture extends EnhanceTestFixture
{
}
Now we need to set up the tables required in our tests.
private $Target;
public function setUp()
{
$tables = new TableHelper();
$tables->CreateCompanyTable();
$this->Target = Enhance::GetCodeCoverageWrapper('CompanyRepository');
}
Before we write a test, let's add a tear-down that removes the table. Because Enhance PHP will catch errors in the tests, we can be sure that this will be run even if the test fails as long as our code can be parsed.
public function tearDown()
{
$tables = new TableHelper();
$tables->DropCompanyTable();
}
Now we have an empty company table that is created and removed after each test (using a SQL script in our Table Helper). We can now write a test for one of our operations against this table. It is important that each test sets up the data it needs, so tests can be run in any order or individually.
public function getByIdWithIdExpectCompany()
{
$company = new Company();
$company->Id = 8;
$company->Name = 'test';
$company->TypeId = 5;
$this->Target->Save($company);
$result = $this->Target->GetById($company->Id);
Assert::AreIdentical($company->Id, $result->Id);
Assert::AreIdentical($company->Name, $result->Name);
Assert::AreIdentical($company->TypeId, $result->TypeId);
}
In this example we create a company and save it in the database (either using an existing method on the data access layer or using a raw SQL script). We then call the data access layer and check that it does what we expect.
If you haven't yet written the "Save" method, you could easily use the following SQL.
INSERT INTO tblCompany (Id, Name, TypeId) VALUES (8, 'test', 5);
Remember, after each test the entire table is removed, so you can start again from fresh for your next test.