Funny Experience...

Discussion of testing theory and practice, including methodologies (such as TDD, BDD, DDD, Agile, XP) and software - anything to do with testing goes here. (Formerly "The Testing Side of Development")

Moderator: General Moderators

McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

Back to the test... I'll just dump Request since it isn't being used. I haven't set any expectations for the result iterator yet, so:

Code: Select all

function test() 
    {
        $phrasebook =& new MockCompetitorsSql($this);
        $phrasebook->expectOnce('findAll');
        
        $row_0 = array('quark'=>'foo');
        $row_1 = array('strangeness'=>'bar');
        $row_2 = array('charm'=>'buff');
        $it =& new MockResultIterator($this);
        $it->setReturnValue('next', false);
        $it->setReturnValueAt(0, 'next', $row_0);
        $it->setReturnValueAt(0, 'next', $row_1);
        $it->setReturnValueAt(0, 'next', $row_2);
        $it->expectCallCount('next', 4);
        $it->expectCallCount('numRows', 1);

        $db =& new MockDatabase($this);
        $db->setReturnReference('getRows', $it);
        $db->expectOnce('getRows');

        $handler =& new PartialCompetitorsListHandler($this);
        $handler->setReturnReference('_CompetitorsSql', $phrasebook);
        $handler->CompetitorsListHandler($request, $db);
        $this->assertIdentical($handler->try(), true);
        $db->tally();
        $phrasebook->tally();
        $it->tally();
    }
I'm assuming that some presentation code will make a numRows call before deciding whether to loop through the list.

So far, in the CompetitorsListHandler, we've only added some code to fetch the data needed for a browser page. This is as far as you can go before applying formatting. We're now at the edge of what I call a Cartesian boundary. These exist at inputs and outputs to the application. They don't quite correspond to the controller / view layers: an input boundary cuts across the controller layer separating input from application controller logic. An output boundary cuts across the view between the data-gathering and the output formatting. Inside these boundaries, the internal "application mind" is blithely unaware of the presentational context. Swapping out an HttpRequest object for a CLI version would allow the app to be easily changed to run on the command line, for example. The Request object is a Gateway translating http or CLI input.

Even if you don't plan on doing anything other than a web app, I think it's still useful to clearly define these boundaries. With data gathering separated from output formatting, you can re-use the same data-gathering code for any kind of output. Also, testing an UI is always a bit tricky; pushing formatting as far out to the edge of the design as possible helps by allowing you to unit-test as much of the logic as you can before being forced to pick up the web tester. At the other end, the Request object, Gateways also make useful points to mock/stub.

Once you've got the data, printing a browser page is as easy as including a template file with some presentational code in the same scope as the data.

Code: Select all

function try() 
    {
        $phrases =& $this->_CompetitorsSql();
        $list =& $this->_db->getRows($phrases->findAll());
        if(false !== $list) {
            include($this->_template);
            return true;
        }
    }
If there were any other data to fetch in addition to the list, this would follow the same pattern of querying domain / data access objects and setting class properties. This is all pretty simple: in more complex examples maybe it could be useful to pass a container around to gather up the data.

This isn't separating out data-gathering and formatting very well but we can come back to that.

You can see we have a problem. We really don't want to output a page in the middle of the test... You could use buffering:

Code: Select all

function test() 
    {
        // ...
        // ...
        // ...
        ob_start();
        $return_value = $handler->try();
        ob_end_clean();
        $this->assertIdentical($return_value, true);
        // ...
        // ...
        // ...
    }
I'm kind of uneasy about that although it does work. It feels like cheating somehow. Note that buffering can interfere with SimpleTest reporting. If it's wrapping an assertion you might miss a fail message.

Another option is to add a print() method to CompetitorsListHandler (partially mock the class to knock it out in the test):

Code: Select all

function try() 
    {
        $phrases =& $this->_CompetitorsSql();
        $this->_list =& $this->_db->getRows($phrases->findAll());
        if(false !== $this->_list) {
            $this->print();
            return true;
        }
    }
    function print() 
    {
        include($this->_template);
    }
If you do knock out the print method in a partial mock, none of the presentational code in the template is being exercised so the expectations on the mock result iterator would have to be changed.

This has a better separation of formatting and data-gathering. You could make print() an abstract method and subclass with different print strategies.
Last edited by McGruff on Fri Aug 05, 2005 9:05 pm, edited 1 time in total.
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

This has been enlightening, especially as even after reading the Mocks v Stubs I wasn't getting the complete difference in how it directs your tests. I've almost finished the domain-out version to where it would sync up with what you've shown. Let me get the most recent test to pass and then I'll post what I've done. I know its switching gears a bit, but perhaps seeing what I've been doing will help identify what I've been doing wrong.
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

I think maybe the key point is to manouevre all the formatting out of the classes and into templates. Once all that presentational stuff drops out you're left with a very simple "fetch some data" problem which is trivial to solve.

Presentation code is almost a different application altogether, on the other side of the boundary. It's usually a minor concern: nothing more complicated than some echo's, loops and the odd conditional. It's the one place I'd be happy using some simple procedural code, mixed in with the html.

The app basically just deals in data. For example, I'd leave it up to the designer to manage shared layout elelments such as headers or footers with Dreamweaver or etc libraries. A format-agnostic php core doesn't have any concept of layout elements as such, just grouped sets of data.
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

PS: the return values for the result iterator above were a mistake. Since the output string isn't being checked anywhere they're not needed. If you include & buffer, the presentation code would make calls to the mock result iterator so we just need a call count expectation. If you knocked out the include altogether you wouldn't even need that.

The unit test is only going so far. It checks that the cast of players are following the script up to a point but ultimately you would also have to web test. Either that or capture buffered output and use the SimpleText lexer to check this in a similar way to the web page content checks. Not something I've looked into.
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

PPS: is that you in the pink dress?
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

McGruff wrote:PPS: is that you in the pink dress?
Nope I'm the guy in the photo
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

McGruff wrote: The unit test is only going so far. It checks that the cast of players are following the script up to a point but ultimately you would also have to web test. Either that or capture buffered output and use the SimpleText lexer to check this in a similar way to the web page content checks. Not something I've looked into.
I guess I've been approaching the divide from the other side. Given the needed data, can it construct the appropriate text representation. Not the appropriate web output, just the appropriate fixed format test string. The the web-test basically devolves to an expectation test ... did the function to generate the list on the data get called.

But this still lets me test from outside the web-test and stay in a pure unit environment until the last possible moment.
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

Wow, well I got my feature working. I haven't attacked it from the request side yet. (This section of the application is still using primitive Page Controller, very procedural script like.) I hope to play with the Handler paradigm shortly, I like where its going, but time pressure and all... Still at least this feature has a decent test harness.

Hopefully I'll be able to refactor it some and at least I feel confident that, assuming I find a way to, I won't break anything. Yay tests!

Five more features to go on the spiral that was supposed to end today... But the time testing now, will be worth it in the long run... And it should get a little easier as time goes on.


But back to the running example, if the query from the PhraseBook had required a parameter, (ie instead of findAll, we had findSome($someRequestParameter) ) the try() function would have to handle the validation of $someRequestParameter as well as ensuring that such a value exists in the DB?

Since if it says "I can't handle this" and returns false, you'd now need a Handler to re-validate it, but confirm that its an non-existant value to display that stay error page, and another handler for reporting invalid requests, right?
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

If you stick a BadRequestSyntax handler at the start of the chain, it can ask if( !$request->hasValidSyntax()) and serve up a 400 status page if not.

The chain is the application controller. Each handler is trying to serve a different kind of page. The logic is just like a switch case. As soon as one handler decides it can handle the request, chain execution stops.
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

How are you configuring the chains? (I guess that's my question over in the Theory and Design forum)

It sounds like your implying that every "action" of the web application might have multiple handlers (are your Handlers basically what PoEAA calls Commands?). While some of the "BadSyntax" type ones could potentially be reused, it seems like a lot of them are going to be custom for the given action.

Seems like you'd have almost all the chains starting with:
Authentication Required > [Authorization Check] > [Validation/Syntax Check] > [Real Handler] > [Un-handleable Catcher]

With only the first and last being relatively re-useable. Writing three short classes for every action seems like its going o cause a massive explosion in the number of classes and configuration tasks.
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

A little piece of procedural code configures the chain (a PageController):

Code: Select all

include('../config.php');
    include(APP_LIB . 'shared/request_gateways/EmptyRequest.php');    
    $application_controller =& new Cor(new EmptyRequest);
    $application_controller->addHandler('path/to/class_file.php', 'ClassName');
    // etc
    $application_controller->execute();
There would usually be a specific Request class per PageController (each request type normally expects a different GPC parameter list). The EmptyRequest class just asserts that there should be no user supplied parameters.

Virtually every http request has a range of possible pages which might be served in response to the request. The idea here is to have one handler per page - although you could possibly cheat a bit and deal with a range of http error status codes in the final handler.

You've got the picture pretty accurately. However the first handler is normally a BadRequestSyntax class. This wants to create a 400 page and might not do much more than check $request->hasValidSyntax(). Bad request syntax might be a hacking attempt though so you could possibly also attempt to profile the attacker (as best you can..), log details, email the webmaster etc. This must come before any code which needs to access GPC: if the request is missing a required parameter, has invalid values, or alien vars, all bets are off.

Authentication might or might not have its own handler. If you want to display a login page with failed authentication it needs its own handler dedicated to creating the page. At other times, failed authentication might affect only the presentation logic ie a home page displays a login box - and so it doesn't need a handler of its own.

Same deal with authorisation: depends on the specific request and what you want to do with it.

There might also be a DbUnavailable handler if, when you can't connect, you want to display an error page (which you probably do). This would be near the start of the chain, possibly right at the start if BadRequestyntax needs to make db queries.

The rest proceeds as you have outlined, above.

That seems to me enough to make a ChainOfResponsibility very useful. It's quite a powerful pattern which makes a good fit for an application controller. Loosely-coupled, chained handlers are very flexible. You can easily pop authentication/authorisation handlers in and out for example. For those wizzard moments, throw a few more handlers into the mix.

I think a CoR framework promotes nice, cohesive classes. Each handler object is only concerned with doing its own job.

The CoR pattern is less useful if you can't identify loosely-coupled classes for the chain, or if there are very many objects in the chain in which case the "switch/case" logic gets to be inefficient.

I'd better post the class - plus test of course :) Unlike the traditional GoF CoR, chained ojects don't call their successors. The key point however is the logical switch/case behaviour where only one handler is ever active.

This isn't really meant to be a finished version. I can't decide whether to just dump all that error stuff. It also needs a new name - SwitchCase is kind of confusing - might suggest alphabetical case-changing.

Code: Select all

/*
    A minor variation on ChainOfResponsibility.
    A chain "manager" (lazy) loads objects rather than each object loading 
    the next in turn. Otherwise the same logical behaviour: only one handler 
    is ever active, ie the first which decides it can handle the event.
*/
class SwitchCase
{
    var $no_handlers_error = 'Who ate all the handlers?';
    var $cannot_handle_error = 'Unable to process the event.';
    var $_object_meta_data = array();
    var $_error_level;    
    var $_handler_parameters;

    /*
        param (mixed) $handler_parameters - all handlers are isntantiated with the same parameters
        param (mixed) $error_level - pass boolean false to turn error-triggering off
    */
    function SwitchCase(&$handler_parameters, $error_level = E_USER_ERROR)
    {
        $this->_handler_parameters =& $handler_parameters;
        $this->_error_level = $error_level;
    }
    /*
        param (string) $class_name
        param (string) $script pipe-separated list if parents to include (include order left to right)
    */
    function addHandler($class_name, $script)
    {
        $this->_object_meta_data[] = array($class_name, $script);
    }
    /*
        return (mixed)
    */
    function execute()
    {        
        $i = 0;
        while( !is_null($handler_meta = array_shift($this->_object_meta_data))) {
            $this->_handler =& $this->_Handler($handler_meta);
            if($this->_handler->try()) {
                return get_class($this->_handler);
            }            
            ++$i;
        }    
        if($i == 0) {
            if($this->_error_level !== false) {
                trigger_error($this->no_handlers_error, $this->_error_level);
            }
            return null;
        }
        if($this->_error_level !== false) {
            trigger_error($this->cannot_handle_error, $this->_error_level);
        }
        return false;
    }
    function &_Handler($handler_meta)
    {
        $files = explode('|', $handler_meta[1]);
        foreach($files as $path) {
            require_once($path);
        }
        $class = $handler_meta[0];
        return new $class($this->_handler_parameters);
    }
}
The test:

Code: Select all

require_once(APERI_LIB . 'control/SwitchCase.php');
require_once(APERI_LIB . 'filesystem/MakeBranch.php');
require_once(APERI_LIB . 'filesystem/DeleteBranch.php');

class TestOfSwitchCase extends UnitTestCase 
{
    var $_handler_parameters = 'foo';

    function TestOfSwitchCase() 
    {
        $this->UnitTestCase();
    }    
    function setUp() 
    {
        // create some handler classes with hard-coded return values
        $meta = array();
        $meta[] = array('truehandler_foo', true);
        $meta[] = array('truehandler_bar', true);
        $meta[] = array('nullhandler', null);
        $this->_makeHandlers($meta);
    }
    function tearDown() 
    {
        new DeleteBranch(TEST_TMP);
    }
    function testWithNoHandlers()
    {
        $cor =& new SwitchCase($this->_handler_parameters);
        $this->assertIdentical($cor->execute(), null);
        $this->assertError($cor->no_handlers_error);
    }
    function testWithNoActiveHandlers()
    {
        $cor =& new SwitchCase($this->_handler_parameters);
        $cor->addHandler('nullhandler', TEST_TMP . 'nullhandler.php');
        $this->assertIdentical($cor->execute(), false);
        $this->assertError($cor->cannot_handle_error);
    }
    function testSingleActiveHandler()
    {
        $cor =& new SwitchCase($this->_handler_parameters);
        $cor->addHandler('truehandler_foo', TEST_TMP . 'truehandler_foo.php');
        $this->assertEqual($cor->execute(), 'truehandler_foo');
    }
    function testMixedChain()
    {
        $cor =& new SwitchCase($this->_handler_parameters);
        $cor->addHandler('nullhandler', TEST_TMP . 'nullhandler.php');
        $cor->addHandler('truehandler_bar', TEST_TMP . 'truehandler_bar.php');
        $cor->addHandler('truehandler_foo', TEST_TMP . 'truehandler_foo.php');
        $this->assertEqual($cor->execute(), 'truehandler_bar');
    }
    function testParameterReferenceIsPassedToHandlers() 
    {
        $cor =& new SwitchCase($this->_handler_parameters);
        $cor->addHandler('truehandler_foo', TEST_TMP . 'truehandler_foo.php');
        $cor->execute();
        $this->assertReference($cor->_handler->_parameters, $this->_handler_parameters);
    }
    function testPipeSeparatedScriptArgIncludesParentClasses() 
    {
        $meta = array();
        $meta[] = array('parent', null);
        $meta[] = array('child', true, ' extends parent');
        $this->_makeHandlers($meta);
        $cor =& new SwitchCase($this->_handler_parameters);
        $cor->addHandler('child', TEST_TMP . 'parent.php|' . TEST_TMP . 'child.php');
        $this->assertEqual($cor->execute(), 'child');
    }
    function testWithNoHandlersAndErrorsToggledOff()
    {
        $cor =& new SwitchCase($this->_handler_parameters, false);
        $this->assertIdentical($cor->execute(), null);
        $this->assertNoErrors();
    }
    function testWithNoActiveHandlersAndErrorsToggledOff()
    {
        $cor =& new SwitchCase($this->_handler_parameters, false);
        $cor->addHandler('nullhandler', TEST_TMP . 'nullhandler.php');
        $this->assertIdentical($cor->execute(), false);
        $this->assertNoErrors();
    }

    #
    # file gen could possibly be made clearer..
    #
    function _makeHandlers($list) 
    {
        new MakeBranch(TEST_TMP);
        foreach($list as $handler_meta) {
            $handle = fopen(TEST_TMP . $handler_meta[0] . '.php', 'wb');
            $contents = call_user_func_array(array($this, '_handlerTemplate'), $handler_meta);
            fwrite($handle, $contents);
            fclose($handle);
        }
    }
    function _handlerTemplate($name, $active, $extends = '')
    {
        if($active) {
            $return_value = 'true';
        } else {
            $return_value = 'null';
        }
        $tpl = '';
        $tpl .= "<?php\n";
        $tpl .= "class " . $name . $extends . "\n";
        $tpl .= "{\n";
        $tpl .= "\tfunction " . $name . "(&\$parameters)\n";
        $tpl .= "\t{\n";
        $tpl .= "\t\t\$this->_parameters =& \$parameters;\n";
        $tpl .= "\t}\n";
        $tpl .= "\tfunction try()\n";
        $tpl .= "\t{\n";
        $tpl .= "\t\treturn " . $return_value . ";\n";
        $tpl .= "\t}\n";
        $tpl .= "}\n";
        $tpl .= "?>\n";
        return $tpl;
    }
}
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

I'm just now getting back to the handler aspect. I think its going ok, I'm still fighting with the interface. I don't like "try" as the method name (as it confliects with exception handling in php5).

I think I'm being pushed into:
canHandle()
execute()

It will slightly complicate the logic in my equivilent of your SwitchCase, ie the nice little while loop you have gets slighty more complex. First loop to find one that answers true to canHandle. Then execute that handler, which will return either a name of a View class or 0 for error handling. But so far so good.... Thanks again.
User avatar
neophyte
DevNet Resident
Posts: 1537
Joined: Tue Jan 20, 2004 4:58 pm
Location: Minnesota

Post by neophyte »

Burrito wrote:I just set my newest goal in life:

to understand what the hell you two talk about in this forum....

DITTO.
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

OK here's the AccessDeniedHandler and tests. I'm not happy with it. The mocking both of the context and the permissions seems way too much "peeking inside the implementation". How do I fix this?

Code: Select all

class AccessDeniedHandler {
  var $_loginRequired;
  var $_perms;
  function AccessDeniedHandler($loginRequired=TRUE,$perms=NULL) {
    $this->_loginRequired=$loginRequired;
    $this->_perms=$perms;
  }
  function canHandle(&$context, $request) {    
    if ($this->_loginRequired) {
      return $context->getUserName()=="" || 
	( $this->_perms!==NULL &&
	  !($this->_perms->isAdequatePermissions($context->getUserPerms())));
    } else 
      return FALSE;
  }

  function execute(&$context, $request) {
    return ($this->_loginRequired && $context->getUserName()=="") ?
      "LoginView" : "InsufficentPermissionsView";
  }
}
The tests (needs some refactoring/splitting to two test cases with more setup/teardown commonality, but...)

Code: Select all

class TestAccessDeniedHandler extends UnitTestCase {
  
  var $context;  
  function TestAccessDeniedHandler() {
    $this->UnitTestCase('Test AccessDeniedHandler');
  }

  function setUp() {
    $this->context = new MockContext($this);
  }
  
  function tearDown() {
    $this->context->tally();
  }

  function testLoginNotRequired() {
    $request = NULL;
    $handler = new AccessDeniedHandler(FALSE);
    $this->context->expectCallCount("getUserName",0);
    $this->assertFalse($handler->canHandle($this->context,$request));
  }

  function testAuthenticated() {
    $request = NULL;
    $handler = new AccessDeniedHandler(TRUE);
    $this->context->setReturnValue("getUserName","nielsene");
    $this->context->expectCallCount("getUserName",1);
    $this->assertFalse($handler->canHandle($this->context,$request));
  }

  function testNonAuthenticated() {
    $request = NULL;
    $handler = new AccessDeniedHandler(TRUE);
    $this->context->setReturnValue("getUserName","");
    $this->context->expectCallCount("getUserName",2);
    $this->assertTrue($handler->canHandle($this->context,$request));
    $this->assertEqual($handler->execute($this->context,$request),
		       "LoginView");
		       
  }

  function testInsufficentAccess() {
    $request = NULL;
    $permissions = new MockPermissions($this);
    $permissions->setReturnValue("isAdequatePermissions",FALSE);
    $handler = new AccessDeniedHandler(TRUE,$permissions);
    $this->context->setReturnValue("getUserName","nielsene");
    $this->context->expectCallCount("getUserName",2);
    $this->context->setReturnValue("getUserPerms",NULL);
    $this->context->expectCallCount("getUserPerms",1);
    $this->assertTrue($handler->canHandle($this->context,$request));
    $this->assertEqual($handler->execute($this->context,$request),
    		       "InsufficentPermissionsView");    
    $permissions->tally();
  }
}
I think I can rip out all the tally's and all the expectCallCounts as that's not really important here. I guess I really only needed stubs. If I do that, I think its less instrusive. I'm also not sure I like the design choice to add the getUserPerms function to the Context, but I guess its what TDD says to do at this point.... What stops you from creating god-classes when mocking? It seems like anytime you need some other piece of data you'll just invent a method for it from some facade. Otherwise you end up mocking an ever widening web of objects....
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

I think I can rip out all the tally's and all the expectCallCounts as that's not really important here. I guess I really only needed stubs. If I do that, I think its less instrusive.
Interaction-based tests are by their nature intrusive but that's the point. You're trying to figure out the interfaces of neighbouring objects in the design, and how these will be used by the object under test.

Stubs just return values on demand but mocks also allow you to set expected call counts and the arguments expected at each method call. The expectations are important: without them, it wouldn't be TDD. With expectations, the test case verifies that the primary object interacts with the mock in a specific way. Stubs don't do this. There's no way to assert that a mock foo() method is being called with the correct number of arguments, or how many times it's called in total.

Expectations also describe the behaviour of the mock under specific conditions, as set up in the tests. (It would be nice if there were some way to automatically extract test methods for a mock object implementation from the primary class's test case; if an expectation is set that mock object Foo should return true from method bar when it's called with a certain parameter, that should also be part of a Foo implementation test case).

Stubs are too dumb to explore a design idea. You've got the interface of a neighbouring object but you haven't explained how it should respond to specific prompts, or what these will be, and you aren't testing that they will actually be made by the primary object.
I'm also not sure I like the design choice to add the getUserPerms function to the Context, but I guess its what TDD says to do at this point....
I might be looking at something like:

Code: Select all

class Context
{
    function hasPermission($name)
    {
        // return the $name key from session, if set
        // obtain value from domain if not & save to session
    }
}
What stops you from creating god-classes when mocking? It seems like anytime you need some other piece of data you'll just invent a method for it from some facade. Otherwise you end up mocking an ever widening web of objects....
What lies beyond the mock object's interface is of no concern to the current test. It might well turn out to be a facade for a whole tree of objects, but you won't know - and don't need to know - until you come to implement the mock. That's the time to activate the god class radar: create new mocks wherever you find a discrete responsibility. If you discover an ever-widening web that's fine - the whole point of the exercise really. Drop a pebble in the pond and watch where the ripples go.
Post Reply