Article

Test Driven Development: Are You Test-infected?

Page: 1 2 3

So, why did I make some of those small (so small, in fact, that you may not even have noticed) design decisions along the way? Why, for example, did I choose to specify some “naughty words” using the constructor of NaughtyWordFilter? The answer is that this felt like the cleanest solution at an API level. It just came instinctively. This is the sort of constant design thought process that developers go through to produce clean, flexible, testable code. Writing the tests encourages you to think carefully about the interface of your code, otherwise you won’t be able to test it very easily.

Often, you’ll want to avoid using real components (which may have their own bugs, or may be awkward to set up) in a test so that you can focus entirely on the behaviour of the SUT. In this case, we use mock objects. Mock objects are objects that look and feel like real objects but are able to replace real components and play their roles in the SUT (they’re often referred to as “actors” or “stubs”). Mock objects are also able to make assumptions about what the SUT will do with them (often referred to as “critics”). This makes them an extremely powerful tool for use within any unit test framework.

SimpleTest provides its own mock object framework, but with other programming languages, you often have to download a separate tool if you want automated mock object generation.

Let’s quickly write our TextFilterChain class. Since this class has dependencies on instances of the TextFilter interface, it presents a prime opportunity for using mock objects.

First, we create a failing test case with a generated mock object. We expect each filter to be invoked:

...  
Mock::generate('TextFilter', 'MockTextFilter');  
 
class TextFilterChainTest extends UnitTestCase {  
 private $_filterChain;  
 
 public function setUp() {  
   $this->_filterChain = new TextFilterChain();  
 }  
 
 public function testEachFilterIsInvoked() {  
   $filter1 = $this->_createMockFilter();  
   $filter2 = $this->_createMockFilter();  
 
   $filter1->expectOnce('filter');  
   $filter2->expectOnce('filter');  
 
   $this->_filterChain->addFilter($filter1);  
   $this->_filterChain->addFilter($filter2);  
 
   $this->_filterChain->filter('foo');  
 }  
 
 private function _createMockFilter() {  
   return new MockTextFilter();  
 }  
}  
 
...

The failure looks something like this:

TextFilterChainTest.php  
1) Expected call count for [filter] was [1] got [0] at [/Users/chris/word_filter/tests/unit/TextFilterChainTest.php line 20]  
 in testEachFilterIsInvoked  
 in TextFilterChainTest  
2) Expected call count for [filter] was [1] got [0] at [/Users/chris/word_filter/tests/unit/TextFilterChainTest.php line 21]  
 in testEachFilterIsInvoked  
 in TextFilterChainTest  
FAILURES!!!  
Test cases run: 1/1, Passes: 0, Failures: 2, Exceptions: 0

The test specified that if two filters are added to the filter chain, the filter chain should call the filter() method on both of them. This hasn’t happened, because we haven’t implemented such a feature yet.

Implementation follows:

...  
 
class TextFilterChain {  
 private $_filters = array();  
 
 public function addFilter(TextFilter $filter) {  
   $this->_filters[] = $filter;  
 }  
 
 public function filter($text) {  
   foreach ($this->_filters as $filter) {  
     $filter->filter($text);  
   }  
 }  
}  
 
...

The test now passes, and we move on to specify what else should happen. Each filter should be given the text we pass in:

...  
 
 public function testFilterInvocationReceivesTextInput() {  
   $filter = $this->_createMockFilter();  
 
   $filter->expectOnce('filter', array('foo'));  
 
   $this->_filterChain->addFilter($filter);  
 
   $this->_filterChain->filter('foo');  
 }  
 
...

This particular test already passes, so we move on. If one filter changes the text, the next should receive the changed value:

...  
 
 public function testChangesAreChained() {  
   $filter1 = $this->_createMockFilter();  
   $filter2 = $this->_createMockFilter();  
 
   $filter1->expectOnce('filter', array('foo'));  
   $filter1->setReturnValue('filter', '***FOO***');  
   $filter2->expectOnce('filter', array('***FOO***'));                                        
 
   $this->_filterChain->addFilter($filter1);  
   $this->_filterChain->addFilter($filter2);  
 
   $this->_filterChain->filter('foo');  
 }  
 
...

The test fails, so we adjust our implementation to make it pass:

...  
 
 public function filter($text) {  
   foreach ($this->_filters as $filter) {  
     $text = $filter->filter($text);  
   }  
 }  
 
...

Finally, we expect the filtered value to be returned from the chain:

...  
 
 public function testFilteredValueIsReturnedFromChain() {  
   $filter = $this->_createMockFilter();  
     
   $filter->expectOnce('filter', array('foo'));  
   $filter->setReturnValue('filter', '***FOO***');  
   
   $this->_filterChain->addFilter($filter);  
     
   $this->assertEqual('***FOO***', $this->_filterChain->filter('foo'));  
 }  
 
...

Faced with the failing test, we adjust our code:

...  
 
 public function filter($text) {  
   foreach ($this->_filters as $filter) {  
     $text = $filter->filter($text);  
   }  
   return $text;  
 }  
 
...

That’s it, we’re done! If all these tests are collected into a test suite, they can be executed in a single test run, automatically. Obviously, the small scale of this project doesn’t make an overwhelming case for using TDD, but when you extrapolate the same technique into a much larger project, the benefits soon begin to speak for themselves.

Getting Test Infected

I’m currently in the process of rewriting a well-known open source software project of mine. Just a few weeks back, I decided that one of my earlier design decisions could have been a little better so I set about a massive refactoring exercise. Without the tests, I would never have dared attempt this exercise, given the scale of it, and even if I had braved it, I wouldn’t have believed that there were no hidden bugs. I had 77 test cases covering that several thousand-line code base at the time, and the comfort offered by those tests was significant, to say the least. Initially, the tests failed due to the scale of the work I was doing, but these failing tests acted as a guide while I changed interfaces, moved code around, and renamed methods. Eventually, one by one, the tests began to pass, until finally all 77 of them passed. I felt reassured that the huge changes I’d just deployed to my code base were non-breaking.

Back to a point I made earlier in this article, though. TDD wasn’t always this satisfying for me. In fact, my first few months with TDD were hell: half of the time, I was fixing up tests that were failing because I changed some implementation detail slightly. Although the system still worked, my tests said otherwise. We call these tests fragile tests. I also spent a lot of time tearing out my hair wondering how to even begin to test a particular part of the system. It’s all too common a story, and it’s a shame to hear that developers have decided not to use TDD as a result of these initial tough experiences. Let me share some tips with you that I’ve learned along the way:

  • Write extremely short, concise test methods—a good rule of thumb is one assertion per test method, but it’s only a rule of thumb!
  • Never—and I can’t stress this enough—test non-pubic parts of the system. Even if you think they play an important role, they’re extremely likely to change with refactoring. TDD focuses on behaviour, not on implementation.
  • Add some abstraction between the test and the SUT. Specifically, create the SUT in the setUp() method of your xUnit framework where possible, and/or create small factory methods for creating the SUT. These factory methods are often referred to as Creation Methods. Creation Methods make it easier to minimize the number of places in which you’ll need to edit your tests if you change the way you need to initialize the SUT.
  • If you feel that you’re repeating yourself when you’re testing a common set of components, consider whether you can refactor to provide an abstract superclass to test some of the common functionality.
  • Opt to use dependency injection when creating components. Doing this significantly increases the test-friendliness of your code—it lets you gain more control through the use of mock objects.

Although these solutions became obvious to me over time, I must admit that they became obvious to many other developers years ago. Gerard Meszaros has an entire 800-page book dedicated to this subject area. xUnit Test Patterns—Refactoring Test Code demonstrates the common approaches developers have found to improve the way they automate tests. If you have some testing experience, it’s well worth a read.

I hope this article was insightful for those who have dabbled in TDD. I hope it’s encouraging for those who have tried TDD in the past, but gave up when faced with similar problems to the ones I dealt with in my early TDD days. More importantly, I hope it provided an interesting read about my experiences with TDD for everybody who made it this far!

And don’t forget you can download the code for the example in this article, along with the tests. Go away and have play. Be warned, though—it’s contagious …

If you liked this article, share the love:
Print-Friendly Version Suggest an Article

Sponsored Links

Rate This Article

  • 1
    Poor
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
    Great

Comment on This Article

Have something to say?

Post A Comment

You need to be a member of the SitePoint Forums to comment on this post. Sign Up

Already a member? Post using your SitePoint Forums account: