Page 1 of 1

TDD Session

Posted: Sun Oct 16, 2005 1:42 am
by stryderjzw
I don't get it.

Well, I do... sort of. I get the gist behind the ideas of TDD. Now, as for execution of TDD, I have no clue.

I heard we start testing top down. Also, TDD can drive the design. But how? I can't seem to figure it out. Looking at other people's fully done testing code doesn't seem to help me dissect it either. I can't get the feel of how development progresses with TDD.

I'm hoping that it's not just me. :lol:

So, I was hoping people at this forum can help me along by doing a TDD session. Looking at some of the other threads, there seems to be interest in this, but progress has stopped. People interested in reading a TDD session can post here as well, asking questions if you have any, along the way.


TDD Session specifications

Problem Domain:
I have a database table of Artists. Users will go to a page and query for a specific Artist. A new page will load with the relevant information on the Artist.

Now a question to begin:

1. Can I start by deciding on using Page Controller? From what I have read, it's ok to "think ahead" like this?
2. Can I have a goal in mind, such as: I kinda know my end product should be some MVC-lite thing with this certain database abstraction, etc... or should I leave that up to my tests?

Everyone's time and help is much appreciated.

Posted: Mon Oct 17, 2005 5:42 pm
by McGruff
I think it's OK to choose at the start whether to go with a Page or FrontController. You do want to avoid looking too far ahead in the design though: as a rule, stick to the next layer of objects.

It's understood that there will be a fair amount of refactoring down the line perhaps to improve the design or to open up a new seam to add a new feature. You want the design to emerge in little steps with TDD & evolve through refactoring (again guided by the tests).

How about we start off with something very simple: just a single class which won't need any mock objects for its unit test. We could go on to look at something more complicated later.

I'll pick something I was working on recently: a PatternIterator. This will be used to find all the instances of a pattern in a string haystack. It's often easier to use a sequence of simple regex's to get information out of a string rather than one big complex one. I want a tool I can use like this:

Code: Select all

$it =& new PatternIterator($haystack);
$it->setPattern('/foo/');
while($match = $it->next()) {
    // do something with $match
}
$it->setPattern('/bar/');
while($match = $it->next()) {
    // do something with $match
}
The key here is that, as each match is found, it will be removed from the target string. That would allow you to parse a string like [foo "a phrase" bar] with /"[^"]+"/ to get the phrases, and then /[\w]+/ to get the single words. If you start off with /[\w]+/ obviously you're going to miss the phrase but once the phrase has been removed it'll work just fine. In this particular case, it wouldn't be hard to do it all in one regex but at other times one-big-regex can get nasty.

First test:

Code: Select all

class TestOfPatternIterator extends UnitTestCase 
{
    function TestOfPatternIterator() 
    {
        $this->UnitTestCase();
    }    
    function test()
    {
        $this->assertTrue(false);
    }
}
I always start off like this simply to make sure all the files are in place: the test case, the class, and the test runner (let me know if you need any help to get that set up). It can be handy to write a little script to generate all the files at the click of a button.

When that's working, the first test for real:

Code: Select all

class TestOfPatternIterator extends UnitTestCase 
{
    function TestOfPatternIterator() 
    {
        $this->UnitTestCase();
    }    
    function testWithNoHaystackOrPattern()
    {
        $haystack = '';
        $it =& new PatternIterator($haystack);
        $this->assertIdentical($it->next(), false);
        $this->assertError('Empty regular expression');
    }
}
This is saying that if no pattern has been loaded, or a target haystack, the next() method should return false - it can't find any matches of course. Also, the class should produce an error. It will be trying to call a php regex function without a pattern.

TDD is all about little steps made against clear requirements. At this point you'd stop writing the test and write the simplest thing which will pass the test. Over to you.

Posted: Wed Oct 19, 2005 12:53 am
by stryderjzw
Thanks very much for your time, McGruff. Much appreciated.

Note - I am using PHP5. I hope that's not an inconvenience.

Code: Select all

class PatternIterator {
	
	private $haystack;
	
	public function __construct($hay) {
		$this->haystack = $hay;
	}
	
	public function next() {
		if ($this->haystack == '') {
			trigger_error('Empty regular expression');
			return false;
		}
	}

}
I guess.... this should work... What should I be thinking of next?

Posted: Wed Oct 19, 2005 1:12 am
by McGruff
Hi.

It seems my test wasn't quite right: it doesn't actually test the regex error after all. This would have been better:

Code: Select all

function testWithNoPattern()
    {
        $it =& new PatternIterator('foo');
        $this->assertIdentical($it->next(), false);
        $this->assertError('Empty regular expression');
    }
That was a very small step - I'm trying to emphasise the interplay between write a little test and write a little code. At this point you've got a next() method and are triggering an error. You did exactly the right thing to hard code the error and the false value. We know this will have to be replaced but it's the simplest thing which gets us through the test. You often do this simply to defer something until later.

However, the next test forces you to start implementing the desired behaviour for real:

Code: Select all

function testNext()
    {
        $it =& new PatternIterator('foo');
        $it->setPattern('/bar/');
        $this->assertIdentical($it->next(), false);
        $it->setPattern('/foo/');
        $this->assertIdentical($it->next(), 'foo');
        $this->assertIdentical($it->next(), false);
    }
And the one after that, the real point of the class, tests that successive regex's can be applied without interfering with each other. This is where you have to earn your money:

Code: Select all

function testSequentialParsing()
    {
        $it =& new PatternIterator('foo "hello world" bar');
        $it->setPattern('/(?<=")[^"]+(?=")/');
        $this->assertIdentical($it->next(), 'hello world');
        $this->assertIdentical($it->next(), false);

        $it->setPattern('/[\w]+/');
        $this->assertIdentical($it->next(), 'foo');
        $this->assertIdentical($it->next(), 'bar');
        $this->assertIdentical($it->next(), false);
    }
The point here is that the second iteration, using the /[\w]+/ pattern, should not match "hello" and "world" - we've already chewed over this part of the target string with a different pattern.

[EDIT: I wrote that in a rush and had to go back and make a few changes - make sure you're using this latest version]

Posted: Wed Oct 19, 2005 1:27 am
by McGruff
PS: I don't use php5 so we'll need to watch out for differences due to reference passing. It's high time I got the manual out and got up to speed, I guess.

Posted: Fri Oct 21, 2005 10:18 pm
by stryderjzw
Sorry for the delay, I had exams. 8)

Code: Select all

class PatternIterator {

	private $haystack;
	private $pattern;

	public function __construct($hay) {
		$this->haystack = $hay;
	}

	public function setPattern($pattern) {
		$this->pattern = $pattern;
	}

	public function next() {
		if ($this->pattern == '') {
			trigger_error('Empty regular expression');
			return false;
		}
		preg_match($this->pattern, $this->haystack, $result);
		if ( ($length = strlen($result[0])) > 0 ) {
			$this->haystack = preg_replace($this->pattern,str_repeat('',$length),$this->haystack,1);
			return $result[0];
		}
		return false;
	}
}
Probably not the most elegant solution, but it appears to work for the tests. So, if it works for the tests, then it should be good, no?

I see how we're adding tests to add functionality to our class, but when do we know to stop? Or how many error tests do we need to add? I'm guessing there's a "comfort level" we should reach?

Thanks!

Posted: Sat Oct 22, 2005 4:31 am
by McGruff
stryderjzw wrote:Sorry for the delay, I had exams.
No problem - I expect these live TDD sessions to take some time.

I was getting some undefined index errors (I'd recommend E_ALL error reporting) but a minor change got everything running green again:

Code: Select all

class PatternIterator {

    var $haystack;
    var $pattern;

    function PatternIterator($hay) {
        $this->haystack = $hay;
    }

    function setPattern($pattern) {
        $this->pattern = $pattern;
    }
    function next() {
        if(preg_match($this->pattern, $this->haystack, $result)) {
            if ( ($length = strlen($result[0])) > 0 ) {
                $this->haystack = preg_replace(
                    $this->pattern,
                    str_repeat('',$length),
                    $this->haystack,
                    1);
                return $result[0];
            } 
        } else {
            return false;
        }
    }
}
stryderjzw wrote:Probably not the most elegant solution, but it appears to work for the tests. So, if it works for the tests, then it should be good, no?
Absolutely. The green bar is all that really matters, within reason. If you've got a deadline to meet you might not always have time to perfect the class, but you'll always have the unit tests to refactor against later.

You'll notice I took the trigger_error lines out. Does it still work? This is actually quite a cute thing about testing: if you've got a hunch that you can make a change but you're not really sure you can just go ahead and experiment. If everything stays green you were right; if not it only takes a moment to undo. Without the tests you wouldn't have the same instant feedback and you'd have to sit down and think it through properly. Sometimes you get away with it; sometimes you don't.

Preg_replace_callback provides another solution:

Code: Select all

class PatternIterator
{
    var $_pattern; 

    function PatternIterator($haystack) 
    {
        $this->_haystack = $haystack;
    }
    function setPattern($pattern) 
    {
        $this->_pattern = $pattern;
    }
    function next()
    {
        $this->bite = false;
        $this->_haystack = preg_replace_callback(
            $this->_pattern, 
            array(&$this, '_setBite'), 
            $this->_haystack, 
            1);
        return $this->bite;
    }
    function _setBite($matches)
    {
        $this->bite = $matches[0];
        return '';
    }
}
stryderjzw wrote:I see how we're adding tests to add functionality to our class, but when do we know to stop?
You probably won't think of everything on the first pass. Tests never provide a 100% proof, but I think you can get as close to that as the amount of effort you're prepared to put in. At least you do have a formal document which describes the behaviour which an implementation must exhibit. You can always add further constraints later as new things come to mind. I find that the discipline of testing makes me think about the details much carefully than I otherwise might have so even the first-pass test case is a huge improvement on none at all.

One general point would be to make sure you test the class with the full range of values it could receive in the wild. If a method takes an integer parameter, I'd usually test with 0, 1 since these can sometimes be special cases. After that, any other number. Very often you're looking for the simplest way to get an inductive proof.

In case you're interested, here's the class and test case for my own PatternIterator (a bit of a work in progress so it might have a bug or two still). I needed a couple more features: the class will be used to parse search terms and I need to get the offsets in order to rank results by word order later. Preg_replace_callback is no good for that but you can iiterate through pattern matches with preg_match if you keep track of the current offset and feed that back into the function.

It's also set up so it can chew through found matches at the same it's iterating over the main target string ie:

Code: Select all

function _mainLoop
    {
        $this->_chewer->setPattern(..etc..);
        while($token = $this->_chewer->next()) {
            $this->_findInToken($token);
        }
    }
    function _findInToken($token)
    {
        $this->_chewer->chewToken($token);
        $this->_chewer->setPattern(..etc..);
        while($foo = $this->_chewer->next()) {
            // do something with $foo
        }
        $this->_chewer->chewHaystack();
    }

Code: Select all

class TestOfPatternIterator extends UnitTestCase 
{
    function TestOfPatternIterator() 
    {
        $this->UnitTestCase();
    }
    function testWithNoPattern()
    {
        $string = 'foo';
        $pattern = '';
        $it =& new PatternIterator($string);
        $this->assertIdentical($it->next(), false);
        $this->assertError('Empty regular expression');
    }
    function testReferencedTarget()
    {
        $string = 'foo';
        $pattern = '/foo/';
        $expected_offset = 0;

        $it =& new PatternIterator($string);
        $it->setPattern($pattern);
        $this->assertIdentical($it->next(), 'foo');
        $this->assertIdentical($it->getOffset(), $expected_offset);
        $this->assertIdentical($it->next(), false);
        $this->assertIdentical($it->getOffset(), null);
        $this->assertIdentical($string, '   ');
    }
    function testCopiedTarget()
    {
        $string = 'foo';
        $pattern = '/foo/';
        $it =& new PatternIterator($string, true);
        $it->setPattern($pattern);
        $this->assertIdentical($it->next(), 'foo');
        $this->assertIdentical($it->next(), false);
        $this->assertIdentical($string, 'foo');
    }
    function testMultipleMatches() 
    {
        $string = 'foo foo';
        $pattern = '/foo/';
        $expected_offset_0 = 0;
        $expected_offset_1 = 4;

        $it =& new PatternIterator($string);
        $it->setPattern($pattern);
        $this->assertIdentical($it->next(), 'foo');
        $this->assertIdentical($it->getOffset(), $expected_offset_0);
        $this->assertIdentical($it->next(), 'foo');
        $this->assertIdentical($it->getOffset(), $expected_offset_1);
        $this->assertIdentical($it->next(), false);
        $this->assertIdentical($it->getOffset(), null);
        $this->assertIdentical($string, '       ');
    }
    function testMultiplePatterns() 
    {
        $string = 'foo bar';
        $pattern_0 = '/foo/';
        $pattern_1 = '/bar/';
        $expected_offset_0 = 0;
        $expected_offset_1 = 4;

        $it =& new PatternIterator($string);
        $it->setPattern($pattern_0);
        $this->assertIdentical($it->next(), 'foo');
        $this->assertIdentical($it->getOffset(), $expected_offset_0);
        $this->assertIdentical($it->next(), false);
        $this->assertIdentical($it->getOffset(), null);
        $it->setPattern($pattern_1);
        $this->assertIdentical($it->next(), 'bar');
        $this->assertIdentical($it->getOffset(), $expected_offset_1);
        $this->assertIdentical($it->next(), false);
        $this->assertIdentical($it->getOffset(), null);
        $this->assertIdentical($string, '       ');
    }
    // just a note for documentation
    function testLookingPatternDoesNotReplaceSuffixOrPrefix() 
    {
        $string = '"foo"';
        $pattern = '/(?<=")foo(?=")/';
        $expected_offset = 1;

        $it =& new PatternIterator($string);
        $it->setPattern($pattern);
        $this->assertIdentical($it->next(), 'foo');
        $this->assertIdentical($it->getOffset(), $expected_offset);
        $this->assertIdentical($it->next(), false);
        $this->assertIdentical($it->getOffset(), null);
        $this->assertIdentical($string, '"   "');
    }
    #!! untested in copy mode
    // usage example in TermsParser, method: _findOrTerms()
    function testFindInSubstring() 
    {
        $haystack = 'foo "hello world" "dial 999" bar';
        $it =& new PatternIterator($haystack);
        $it->setPattern('/"[^"]+"/');
        $token = $it->next();

        $it->chewToken($token);
        $it->setPattern('/[a-zA-Z]+/');
        $this->assertIdentical($it->next(), 'hello');
        $this->assertIdentical($it->getOffset(), 5);
        $this->assertIdentical($it->next(), 'world');
        $this->assertIdentical($it->getOffset(), 11);
        $this->assertIdentical($it->next(), false);
        $this->assertIdentical($it->getOffset(), null);
        
        $it->chewHaystack();
        $token = $it->next();
        
        $it->chewToken($token);
        $it->setPattern('/[a-zA-Z]+/');
        $this->assertIdentical($it->next(), 'dial');
        $this->assertIdentical($it->getOffset(), 19);
        $this->assertIdentical($it->next(), false);
        $this->assertIdentical($it->getOffset(), null);
        
        $it->chewHaystack();
        $it->setPattern('/[\w]+/');
        $this->assertIdentical($it->next(), 'foo');
        $this->assertIdentical($it->getOffset(), 0);
        $this->assertIdentical($it->next(), 'bar');
        $this->assertIdentical($it->getOffset(), 29);
        $this->assertIdentical($it->next(), false);
        $this->assertIdentical($it->getOffset(), null);
        
        // note that the token will always be replaced by empty space in the 
        // haystack, even if its contents have not been completely chewed
        $this->assertIdentical($haystack, str_pad('', 32));
        $this->assertNotIdentical($haystack, 
            str_pad('', 24) . '999' . str_pad('', 5));
    }
}

Code: Select all

class PatternIterator
{
    var $_match_after_offset = 0;
    var $_current_match_length = 0;
    var $true_offset;
    var $_haystack;
    var $_pattern;
    var $_token_offset = 0;

    /*
        param (string)  $haystack
        param (boolean) $copy: by default, the haystack is passed by ref
                        and pattern matches will be replaced with the 
                        corresponding number of spaces; $copy === true
                        leaves the original haystack unaltered
    */
    function PatternIterator(&$haystack, $copy = false) // implements Iterator. Probably.
    {
        if( !$copy) {
            $this->_haystack = $this->_target_ref =& $haystack;
        } else {
            $this->_haystack = $this->_target_ref = $haystack;
        }
    }
    /*
        param (string)  $pattern
    */
    function setPattern($pattern) 
    {
        $this->_pattern = $pattern;
        $this->_reset();
    }
    /*
        return (string/false)
    */
    function next()
    {
        if(preg_match(
            $this->_pattern, 
            $this->_haystack, 
            $matches, 
            PREG_OFFSET_CAPTURE, 
            $this->_match_after_offset)) {
            $match = $matches[0][0];
            $match_offset = $matches[0][1];
            $this->_update($match, $match_offset);
            return $match;
        } else {
            $this->true_offset = null;
            $this->_target_ref = $this->_haystack;
            return false;
        }
    }
    function _update($match, $match_offset)
    {
        $this->_current_match_length = strlen($match);
        $this->_removeMatchFromHaystack($match_offset);
        $this->_match_after_offset = 
            $this->_current_match_length + $match_offset;
        $this->true_offset = $match_offset + $this->_token_offset;
    }
    function _removeMatchFromHaystack($offset)
    {
        $this->_haystack = 
            substr($this->_haystack, 0, $offset) . 
            str_pad('', $this->_current_match_length) . 
            substr($this->_haystack, $offset + $this->_current_match_length);
    }
    /*
        return (array)
    */
    function getOffset()
    {
        return $this->true_offset;
    }
    function chewToken($token)
    {
        if( !isset($this->_cache)) {
            $this->_cache = array();
            $this->_cache['haystack'] = $this->_haystack;
            $this->_cache['pattern'] = $this->_pattern;
            $this->_cache['current_offset'] = $this->_match_after_offset;
        }
        $this->_haystack = $token;
        $this->_token_offset = 
            $this->_match_after_offset - $this->_current_match_length;
        $this->_reset();
    }
    function chewHaystack() 
    {
        if(isset($this->_cache)) {
            $this->_token_offset = 0;
            $this->_haystack =& $this->_cache['haystack'];
            $this->_pattern = $this->_cache['pattern'];
            $this->_match_after_offset = $this->_cache['current_offset'];
            unset($this->_cache);
        } else {
            // ??
            trigger_error('no cache set');
        }
    }
    function _reset()
    {
        $this->_match_after_offset = 0;
        $this->_current_match_length = 0;
        $this->true_offset = null;
    }
}

Posted: Sat Oct 22, 2005 10:21 pm
by stryderjzw
I was getting some undefined index errors (I'd recommend E_ALL error reporting) but a minor change got everything running green again:
I was able to get rid of them. Just to reaffirm, it's good to get rid of those E_Notice as well, right? I remember reading something about PHP5 and error notices, can't remember if it's important tho. :lol:
You'll notice I took the trigger_error lines out. Does it still work? This is actually quite a cute thing about testing: if you've got a hunch that you can make a change but you're not really sure you can just go ahead and experiment. If everything stays green you were right; if not it only takes a moment to undo. Without the tests you wouldn't have the same instant feedback and you'd have to sit down and think it through properly. Sometimes you get away with it; sometimes you don't.
I had to change:

Code: Select all

$this->assertError('Empty regular expression');
to

Code: Select all

$this->assertErrorPattern('/Empty regular expression/i');
But yeah! I am pleasantly surprised that I can take out those lines. I can see how tests help in this way. I guess that's like cleaning up the code.

So, now that I see one class in the process of TDD.... how does it lead to design? I have heard the saying that Mock Objects with TDD can drive design of an application?

(Unless I'm getting too far ahead of myself... :wink: )

Posted: Sun Oct 23, 2005 11:48 pm
by McGruff
In an OOP design there will be a range of co-operating objects. With TDD you can jump in anywhere to write a class - test first, of course. In the test, you can use mocks in place of any neighbouring objects. Setting expectations for the mock (number of method calls, arguments passed at each method call, and return values) allows you to go ahead and test the primary class even though the objects with which it interacts don't exist. That's the design part. You create whatever interface you want for the mocks and this, along with the behaviour which you've described in the expectations, provide a blueprint for real classes which will replace the mock. These in turn might use mocks in their own test cases and so on and so on.

With TDD you tend to start at the top and the design ripples out from the point at which you started. It avoids "big upfront design" instead staying closely focussed on the requirements for each class. You probably don't know enough to make good design choices at the start - with TDD you don't have to.

This isn't the only testing style but it's certainly something you should try. It's a nice way to work.

If you like, you could make a start on the artists list example you mentioned in the first post. Let me know if you need any help.

Posted: Mon Oct 24, 2005 8:27 pm
by stryderjzw
Thanks, McGruff, for the guidance. I'm going to try to work on a project using TDD, I'll post more if I have questions.

My main issue is that I heard you should read books like PoEAA (in progress), and also I heard so much about MVC, Registry patterns and read all those Sitepoint forum discussions, that it seems like I got all these "classes/design" in my head already before I start anything.

I've been trying to get my head around these advanced concepts that all the code I write seem to already be set in stone and be overcomplicated, instead of writing just enough code to run against a specification. I think my head is just up in the clouds with all these concepts, but putting them together in a usable application has been the hardest part for me. I am hoping TDD can sort of bring me back to the ground.

Posted: Mon Oct 24, 2005 9:53 pm
by McGruff
TDD is excellent for keeping your feet on the ground. The key is that you always have well-defined requirements to code against.

Design patterns are also very important; you need to absorb as many as you can since these provide a wealth of ideas about how to solve programming problems. Some of them - object-relational patterns like Associative table Mapping or presentation layer patterns like Page/FrontController - are standard ways to solve a particular problem. If you know them, you can spot situations where they apply and rattle off the classes fairly quickly. At other times you'll need to figure out some possible solutions yourself and then pick the best one. Studying patterns gets you thinking about motivations and consequences. If need be, you should adapt patterns to fit your problem rather than the other way around - or invent new ones of your own.

It isn't half as hard as it looks on the first read. Many of the patterns in PoEAA took a while to sink in when I was taking my first steps with OOP but one day it just goes click. This is one of the essential texts; it's stuffed full of useful ideas and analysis.

In some ways OOP is like a mix of engineering and philosophy. Objects provide simple interfaces which hide more complicated internal machinery. The PatternIterator for example provides a simple tool which simplifies other classes which make use of it. Each class is a cog in the machine with a specific job to do. Each is a concept which has a place in a bigger picture and the first, fundamental step is relating responsibilities to the standard presentation / domain / data access layering. That's the big one. Overall the goal is to isolate different areas of functionality so that changes made in one part of the system don't rippple through the whole app - this also makes it easier to test.

It takes time to learn and you'll probably have to be prepared to tear things up and start again more than once. It can feel like you're getting nowhere but each time you'll be learning a little bit more. It's worth it in the long run if you're serious about learning to program. The skills you learn will be applicable to other languages as well as php. It's just a much, much easier way to work once you've got the hang of it. You'll also be able to pitch for the best jobs - there are some where agile methods are a requirement, even in php.

Anytime you want to have a go at working through the artist - or another - example let me know. I'm new at this so I'm not sure what's the best way to do it. You could lead the way and I can comment on each step, or I could try taking the lead one test at a time like we did above.

Posted: Wed Oct 26, 2005 1:26 am
by stryderjzw
It takes time to learn and you'll probably have to be prepared to tear things up and start again more than once. It can feel like you're getting nowhere but each time you'll be learning a little bit more.
That's exactly what I'm feeling, but I'm learning and getting there. Thanks for all the advice.

I will start in a few days with my artists example and see where it takes me.