PHP Objects, Patterns and Practice (3rd edition)

(Barry) #1

CHAPTER 18 ■ TESTING WITH PHPUNIT


Tests add a number of costs to your development. As you build safety into the project, for example,
you are also adding a time penalty into the build process that can impact releases. The time it takes to
write tests is part of this but so is the time it takes to run them. On one system, we may have suites of
functional tests that run against more than one database and more than one version control system. Add
a few more contextual variables like that, and we face a real barrier to running the test suite. Of course,
tests that aren’t run are not useful. One answer to this is to fully automate your tests, so runs are kicked
off by a scheduling application like cron. Another is to maintain a subset of your tests that can be easily
run by developers as they commit code. These should sit alongside your longer, slower test run.
Another issue to consider is the brittle nature of many test harnesses. Your tests may give you
confidence to make changes, but as your test coverage increases along with the complexity of your
system, it becomes easier to break multiple tests. Of course, this is often what you want. You want to
know when expected behavior does not occur or when unexpected behavior does.
Oftentimes, though, a test harness can break because of a relatively trivial change, such as the
wording of a feedback string. Every broken test is an urgent matter, but it can be frustrating to have to
change 30 test cases to address a minor alteration in architecture or output. Unit tests are less prone to
problems of this sort, because by and large, they focus on each component in isolation.
The cost involved in keeping tests in step with an evolving system is a trade-off you simply have to
factor in. On the whole, I believe the benefits justify the costs.
You can also do some things to reduce the fragility of a test harness. It’s a good idea to write tests
with the expectation of change built in to some extent. I tend to use regular expressions to test output
rather than direct equality tests, for example. Testing for a few key words is less likely to make my test fail
when I remove a newline character from an output string. Of course, making your tests too forgiving is
also a danger, so it is a matter of using your judgment.
Another issue is the extent to which you should use mocks and stubs to fake the system beyond the
component you wish to test. Some insist that you should isolate your component as much as possible
and mock everything around it. This works for me in some projects. In others, though, I have found that
maintaining a system of mocks can become a time sink. Not only do you have the cost of keeping your
tests in line with your system but you must keep your mocks up to date. Imagine changing the return
type of a method. If you fail to update the method of the corresponding stub object to return the new
type, client tests may pass in error. With a complex fake system, there is a real danger of bugs creeping
into mocks. Debugging tests is frustrating work, especially when the system itself is not at fault.
I tend to play this by ear. I use mocks and stubs by default, but I’m unapologetic about moving to
real components if the costs begin to mount up. You may lose some focus on the test subject, but this
comes with the bonus that errors originating in the component’s context are at least real problems with
the system. You can, of course, use a combination of real and fake elements. I routinely use an in-
memory database in test mode, for example. This is particularly easy if you are using PDO. Here’s a
simplified class that uses PDO to speak to a database:


class DBFace {
private $pdo;
function __construct( $dsn, $user=null, $pass=null ) {
$this->pdo = new PDO( $dsn, $user, $pass );
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}


function query( $query ) {
$stmt = $this->pdo->query( $query );
return $stmt;
}
}


If DBFace is passed around our system and used by mappers, then it’s a simple matter to prime it to
use SQLite in memory mode:


public function setUp() {

Free download pdf