Tell don't ask we have all heard it. Yet it is one of the most violated principles. It is also one of the most important principles to follow if you want a design that is flexible to change.
I have often heard people say, "... but it makes Test driving very hard". I disagree - what it does is stop you from testing at too granular a level and focus on testing behaviours rather than interactions.
Enough postulating ... here is an example taken from my current work. You have an interface to an external system - you have nicely encapsulated it into your own class and throw an exception if, for example, there are infra level issues. Once the exception is raised - you let it percolate to the top and create an alert to inform operations.
The diagram below depicts a more usual approach I've seen:
Chances are that we test drove the Infra Service knowing that it needs to have all the right details required for us to raise the Alert. However, we are not testing that the right Alert is created because that is not the responsibility of the Infra Service. Hence we poke a few holes in the Infra Exception class and assert that the exception is created with the right details.
Once we move to implementing the A Service we have all the relevant details exposed from the exception so we retrieve these details and create the Alert. Interesting that the real behaviour here is:
create alert with the right details if there is an infra error
create exception with the right details if there is an infra error and then create alert from exception with the right details
Now lets looks at the alternative. Lets say we drove the test from the outside, i.e. A Service, and did not mock the Infra Service but instead mocked the External System API.
We end up with a tell-don't-ask approach where we are testing that the right Alert is created when there is an infra error. We also end up with fewer interactions and a more flexible design. If for example we decided to encapsulate the external system some other way, our tests will not need to change.
Although testing at this level provides us with far less brittle tests, the disadvantage is that parts of the implementation is further way. On the other hand there are far fewer reasons why a test may break during refactoring.
Finding the right level to test is not a science. We need to take complexity of the unit-under-test into account. The rule of the thumb is that we should test from outside and mock external boundaries until the tests get too complex and only then get more granular for the more complex parts striving to maintain flexible tests and simple design.
Mash is a pragmatic software craftsman always looking to improve his software creation skills and helping others do the same. He firmly believes that a well-rounded software craftsman must have a keen interest in all aspects of software creation, including; process, people, technology, user experience, development, operation, maintenance, and social impact. He relishes the daily challenges that Codurance brings to him–stretching his existing knowledge and expertise allowing him to constantly grow as a professional.
Mash is an advisor and a leader. During his diverse career, he has succeeded in invigorating large ailing software projects as well as creating highly effective software teams and departments. His broad and deep technical knowledge, organisational skills, craft focus, and empathy to people involved have been integral to his success. He has worked in many roles for charities, investment banks, consultancies, government, media and cloud providers. He prides himself at being a hands-on software developer and believes that software development skills are very hard to learn and the best way to maintain them is to apply them.All author posts
Software is our passion.
We are software craftspeople. We build well-crafted software for our clients, we help developers to get better at their craft through training, coaching and mentoring, and we help companies get better at delivering software.