Testdriven Contexts

263 views
Skip to first unread message

featurefabrik

unread,
Aug 31, 2013, 6:38:58 AM8/31/13
to clean-code...@googlegroups.com, ja...@featurefabrik.de
Hello Clean Code Discussion Board.

I am currently a little bit stuck with the concept of TDD applied to the use-case-driven approach.

Normally, my usecases do have a structure of this kind:

class CreateEvent
 
Input   = Request.build(:creator_name, :event_date, :event_name)
 
Success = Response.build(:event_code)
 
Failure = Response.build(:error)

 
def initialize(input)
   
@creator_name = input.creator_name
   
@event_date   = input.event_date
   
@event_name   = input.event_name
 
end

 
def call
   
if event_valid?
      save_event
     
Success.new(:event_code => event_code)
   
else
     
Failure.new(:errors => errors)
   
end
 
end
 
 
private
 
def event_valid?
    event_validator
.valid?
 
end
 

  def event_validator
   
@event_validator ||= EventValidator.new(@event)
 
end

 
def event
   
@event ||= event_gateway.build(
     
:name => @event_name,
     
:date => @event_date,
     
:creator => creator
   
)
 
end

 
def event_gateway
   
@event_gateway ||= EventGateway.new
 
end

 
def creator
   
@creator ||= user_gateway.detect_by_name(@creator_name)
 
end

 
def user_gateway
   
@user_gateway ||= UserGateway.new
 
end

 
def save_event
   
@event = event_gate.save_and_reload @event
 
end

 
def event_code
   
@event.code
 
end

 
def errors
    event_validator
.errors
 
end


 
class EventValidator < Validator
   
# ...
 
end
end

The biggest problem with this for me is the way these kind of classes evolve.

I really don't know, how to properly test those usecases without leaking too much of implementation details. Also the "most degenerate case" is hard to find for me, since this usecase does not really resemble an algorithm that can be evolved. It is more a plain old crud operation.

The problem I run into is also, how should I test it? I do see (and tried) two approaches here:

Approach 1:
Mocking the *Gateway classes. This is clean, since I can write the test for the specific use case withour branching my brain into first implementing the sepcific gatway methods. Kind of the "programming by wishful thinking", promoted by Corey Haines.
In a second iteration I could testdrive the necessary methods on my gateways.
But I always get some kind of a bad feeling when mocking basic internals of my program. I have the strong feeling that this can lead to rigid testsuits on the long run.

Approach 2:
Using the generic MemoryDataMapper behind the surface, instead of the database. This boils down to a config setting, which datamapper instance should be used by the gateway instances in the tested application. This feels from a viewpoint of stability way better, since the real thing is tested here. But tesdriving this kind of approach does suck somehow. I write a failing test for a use case. Then I see that I need a gateway method, so I hop over (while still red) to the gateway spec and class and testdrive the needed methods. After that I hop back to the red use case and continue where I left of. This branch in thinking feels like a total productivity killer.

So, both approaches do have a really bad taste to me, and I still have a hard time explaining exactly why. Do others see the pain? Or am I just overthinking here?

I'd be curious about insights from other experts!

So far,
have a nice day and greetings from middle Germany
Jakob

Roberto Guerra

unread,
Aug 31, 2013, 10:09:09 AM8/31/13
to clean-code...@googlegroups.com, ja...@featurefabrik.de
I would just use mocks for this case. The Gateway sits on the edge, and it can be a third party interface. I would treat it like that. Also, I don't see why you have to TDD the MemoryDataMapper. I'm probably too lazy and only test the public interfaces. Unless your MemoryDataMapper will be used by third parties, then yeah, I would say test it. 

featurefabrik

unread,
Sep 1, 2013, 4:28:03 AM9/1/13
to clean-code...@googlegroups.com, ja...@featurefabrik.de
I should perhaps say a few more words about the internal structure. The Gateway classes aren't an external adapter. They are internal units of the system which map high-level query and command methods (like for example EventGateway#find_latest_by_category) to low-level methods on a generic data-mapper implementation. These generic data-mappers are provided by "main" (could be an SQL data-mapper, an Redis data-mapper or even the simple Memory data-mapper without any persistence).

For the sake of performance, I currently use the real gateways in my tests with a simple data mapper that maps all data to a hash in memory. This way I can spare any kind of database setup or stuff like that and the tests stay really fast.

My concerns about the second approach was more the necessary "context switch", for implementing the desired functionality.

You write a test that describes the simplest usage of the use case, than, before you can implement the use case itself, you have to switch first to the Gateway and its specs and implement the necessary query or command queries. After that you can code further on the use case. This just does not feel like TDD at all! And I am wondering how guys like you test-drive such complex structures as use cases. Since those use cases seem to me like an essential data-structure in clean architecture, I feel a strong urge to test-drive those little guys! :)

The problem I have with mocks is relatively simple. If you use mocks of essential components in ruby, you generate an ugly dependency. Every time you change the public interface of one component in your system, the next thing you have to do is to find every mocked occurrence of this component and update the mocks. I really, really  often ran into problems with this approach, simply by missing one mock. If you do not have a real strong integration test-suite, such errors could propagate into production in the worst case.
 
Greetings
Jakob

Uncle Bob

unread,
Sep 4, 2013, 11:42:31 AM9/4/13
to clean-code...@googlegroups.com, ja...@featurefabrik.de
I'm in the "approach 1" camp.  Mock the gateways because the gateways sit on the boundary between the semantics of the business rules, and the structure of the business data.  

Make those mocks _simple_.  Don't put lots of processing in them.  And try very hard to make the method names in the gateways independent of implementation.  If you find that interface changes to the gateways are breaking your tests, rethink the gateway design.

featurefabrik

unread,
Sep 5, 2013, 2:21:13 AM9/5/13
to clean-code...@googlegroups.com, ja...@featurefabrik.de
Thanks for the reply!

So far I do also understand these Gateways as sitting on this boundary.
In the application I worked on by now, they never had anything more complex than the obligatory UserGateway.detect(user_uid) and UserGateway.save(user) / UserGateway.save_and_reload(user) or methods like TeamGateway.detect_by_name(name) or UserGateway.select_by_team(team).

Do you consider even those methods as too complex? Or was this more of a general advice about the complexity of gateways.

I totally get the point, but some of our applications are heavy data and query centralized, like participant management systems for events and stuff like that. 50% to 60% of the usecases implement some kind of queries about the internal user/participant stock

This is the reason, we often have changing requirements that implicate changes to the gateway interfaces. Perhaps it is the naming of the gateway methods, that brings that interface change?

Do yout have general rules of thumb for naming methods on the gateways? We use detect_* for single finder and select_* for all methods that emit a collection of result. Often, the gateway ends up with a load of methods like detect_by_first_name, detect_by_email a.s.o. Feels kind of awkward everytime.

Roberto Guerra

unread,
Sep 5, 2013, 8:48:17 AM9/5/13
to clean-code...@googlegroups.com, ja...@featurefabrik.de
Sounds like you need to break your Gateways into smaller pieces. I have a similar project written in Python where the 'gateways' are an entirely separate package. These have mainly integration tests just for my own sanity to make sure the queries are working. So, for example, if I have to generate multiple reports related to patients in a EMR, I wouldn't have one big Patient Gateway. Instead, I would probably have a PatientsVisitGateway, a PatientsPrescriptionsGateway, a PatientsDiagnosesGateway, etc. Then it is much easier to mock these gateways in the boundaries, and we can change much faster because the gateways scope are limited.

featurefabrik

unread,
Sep 5, 2013, 2:23:28 PM9/5/13
to clean-code...@googlegroups.com
Hi!
That sounds like an interesting approach. I always have one gateway per domain entity. So, for example a UserGateway which emits User entities and a UserProfileGateway which emits UserProfile entities.

I never really thought about another partitioning, like yours. Would you mind to mention a few use-cases of the EMR application and which gateways and gateway-methods they are specifically using? I am sure, this would kindly help me understanding your approach correctly.

Thanks for your time!

Uncle Bob

unread,
Sep 9, 2013, 3:59:18 AM9/9/13
to clean-code...@googlegroups.com
If you are using a dynamically typed language, then adding a new method to a gateway interface is not much of a problem, since there is no formal declaration of that interface.  In static languages like java, C#, and C++ however you must add the new method declarations to the gateways, and force all clients of those gateways to recompile, even those that don't care about the new methods.

You can get around this two ways.  

1. Use interface segregation.  Segregate the query methods into interfaces, each of which are used by different clients.  This the number of clients affected by a new method under control.
2. Pass string that names the query, instead of using a method name.  This sacrifices type safety but gives you the same advantage that a dynamically typed language gives you.  The query names are not part of the formal declaration.

As a side note regarding point 2: The string shouldn't be SQL.   It should just be a simple name, similar to the name you would have given to the method.

And remember, even though point 2 sacrifices type safety, you still have your unit tests.  So the code you write won't be misspelling those strings.  <grin>.

Scott Burleigh

unread,
Sep 10, 2013, 6:29:12 PM9/10/13
to clean-code...@googlegroups.com, ja...@featurefabrik.de
Another approach you could think about is a generic specification.  For instance

  
class ProductGateway
  def add_product(product)
    //add method
  end

  def find_by(specifications)
    products.select{|p| satisfied_by_specs(specifications, p)}
  end

  def satisfied_by_specs(specifications, product)
    specifications.all? do |spec|
      spec.is_satisified_by?(product)
    end
  end  

end

class MaterialSpecification
  def initialize(material)
    @material = material
  end

  def is_satisified_by?(product)
    product.built_with?(@material)
  end
end

class ProductIdentifierSpecification
  def intitialize(productId)
    @productId = productId
  end

  def is_satisified_by?(product)
    product.productId == @productId
  end
end

C# is my primary language so I'm sure I didn't do ruby any justice in my example, but hopefully you can see what I meant.  

featurefabrik

unread,
Sep 18, 2013, 5:23:56 AM9/18/13
to clean-code...@googlegroups.com
Segregation of the gateways seems like the way to go. I do like the approach of further partitioning. The idea of a query like interface was also for discussion some time ago, but we decided against it.
Maybe, we will give it a try again :)

featurefabrik

unread,
Sep 18, 2013, 5:26:56 AM9/18/13
to clean-code...@googlegroups.com, ja...@featurefabrik.de
I do like your approach of specifications, since it adds a nice shift in mind. Speaking of queries is so close to persistence and databases whereas specifications just sound nicely generic.

I'll perhaps try a similar approach in future gateway implementations, thanks a lot!

And for your information, you leveraged the power of ruby in an appropriate manner. I wouldn't have taken any notice, that your main language is not ruby. So don't worry :)
Reply all
Reply to author
Forward
0 new messages