It’s quite difficult to test inquisitive code. Very often the results of running a piece of code can only be found in another object. Therefore a test must use a separate entity, which we typically call a “spy,” to monitor the external object and validate that it was called correctly. This adds unnecessary complexity to the test and it means more objects are collaborating than need to be.
When objects are assertive they have everything they need to fulfill their tasks. If they don’t have the facilities to do what they need to do directly, they delegate other objects to do it for them.
Delegating to another object to perform a function is very different than being inquisitive with other objects. The difference has to do with who has the responsibility. Inquisitive code gives up responsibility and assertive code keeps it. Because assertive code is responsible for itself, it’s straightforward. Tests test the responsibility of code and can remain mostly autonomous. This simplifies testing scenarios quite a bit.
When tests are assertive and based on acceptance criteria, the code that’s written to make those tests pass is also assertive. It comes down to writing code in order to implement a feature and making an acceptance criteria pass versus implementing a specification. Implementing a specification can be vague but when code is assertive and has well-defined acceptance criteria, it’s straightforward to write tests around it. This means we know when we’re done and developers spend less time gold-plating and more time implementing valuable features.
Sometimes, writing a test for a behavior is the main clue that tells me I put the behavior in the wrong place. If my test requires spies, which are mocks that observe objects that are not the objects under test, then that almost always tells me I’ve put the behavior in the wrong place. Instead of trying to use a spy in my test I’ll try to redesign my code so that the behavior is in a better place.
How do we deal with inquisitive code? The answer once again is to refactor. The main refactorings to support assertiveness in code are move method, extract method, and extract class.
Very often, objects have the wrong behavior or they have too much behavior. The way to deal with that is through a form of mitosis. Quantum physicists have a term that comes to mind for this. They call it “spooky action at a distance.” Physicists use the term to explain or describe quantum effects but I think it’s also an apt term for inquisitive or unassertive code.
When you want to divide the object up into two or more other objects there are various techniques for doing this. One approach is to separate out the data of an object in terms of its usage. If one set of methods uses one set of data and another set of methods uses another set of data, then you can typically extract those two sets into two separate classes. This may or may not make sense. Fortunately, I have come across a rule of thumb that helps me in situations like these, and in fact, in most situations. Whenever I have a tradeoff to make in programming, a choice between one implementation or another, I simply ask myself which approach is easier to test.
This is an important question both philosophically and practically. It’s important practically because I believe in testability and I don’t consider code written unless it also includes automated tests that exercise its behavior and can validate it works as expected. But this is also important philosophically because as you may have noticed there is a strong relationship between these code qualities and testability.
As we improve each one of these code qualities, we’re also improving the testability of our code. This is much more than mere coincidence. One of the most important aspects to consider when developing software is how testable the design is, or how testable the implementation of the design is. Understanding that question more than anything else has led me to understand what good software development is all about. This one singular idea has been a key distinction that has helped me identify better ways of building software. Use it well.
Previous Post: « Pathologies of Inquisitive Code
Next Post: Quality Code is Nonredundant »