Thoughts on Test Driven Development
Recently I've been to a workshop about Test Driven Development, held by Jason Gorman. I don't consider myself as an expert in TDD, but the concept has started becoming more familiar to me thanks to other developers I met during my career. This workshop definitely cleared my mind about some matters (matters that go beyond red-green-refactor cycle), which I'd like to share with you.
- Tests should ALWAYS be valid: you're not allowed to change a passing test you already written while developing a feature. If you have to do this, it means that you didn't understand properly what your class should do at the beginning. Of course, it is appropriate to change tests when the requirements change, but that's another story.
- Never refactor while a test is still failing: you haven't implemented your logic yet, and refactoring (still a great practice!) should be done only when you have passing tests as safety net (and yes, that also means IDE refactoring).
- While writing a test, assert first: while writing a test, the first thing that should be written is the assert. This way, your test goal will be clear, and only needed given/when clauses will be written.
- If you know your tests need collaborators, you can declare them: if you already discovered from earlier tests, or from requirements, that the class you're testing need to interact with some collaborators, you're allowed to define them (and define their behaviour while writing a specific test).
- You don't need Big Up-Front Design, you just need enough: nobody said that design is a bad practice, although some agile practitioners may have said or believed that (including myself in the past). You should only design what's necessary (this article tells a story I pretty liked), and using a method such as Class Responsibility Collaboration cards you can identify the classes and interactions needed in your software. You can then use these classes and interactions to start writing tests.
Lastly (and most importantly), as all practices mastering test driven development takes time - a common myth is that by starting TDD all benefits brought by it will immediately be visible. While it will become natural for you to code in this way, test driven develpoment has got some kind of steep learning curve. At the beginning everything will seem more complex, and probably you will take more time to write your code than it would have taken without using this approach. However, your software will be simpler, more maintainable and more orthogonal. Plus, as your software will always be correct per requirements (but not bug free), the developers and the business will have more confidence on working/releasing without spending weeks on acceptance tests, significantly speeding up the delivery process.
Has your software got any broken windows?
The broken windows theory it's a theory that has been developed after some studies on criminology. It has been observed that if you park a car in a not-so-safe area, if all its glasses are kept intact, the incidence of act of vandalism or crimes on that care is much lower than if that car has one broken glass, even as small as a rear view mirror. In my personal experience, this applies also to different social behaviour (for example, what happened in my hometown some years ago).
On a more interesting note, however, this theory usually applies also to software development. Keeping your software at a high quality is definitely a challenging task, although this is made easier by using techniques such as Continuous Integration and Test Driven Development, to name a few.
But, what happens if broken windows start appearing in your software? Think about a poor designed class, like a god object, or methods which are not thoroughly tested. It's in the human nature starting adapting to that "chaotic" situation, and it's very likely that other developers will start adapting to the situation by writing bad code. As a result, quality will drop very quickly, and this is just because somebody started breaking windows in the code.
As craftsmen, we believe that we don't want only to write working software, but also to write well crafted software. A part from fixing the broken windows - with processes such as refactoring - we should mentor our team members to let them understand the problem, its importance that goes beyond the scope of that single method or class, and make sure this doesn't happen again. Pair programming, code reviews, automated code coverage checks are some of the methodologies/tools you can use to mentor people. Fixing broken windows is possible, but it's more costly and just by fixing probably you'll have to do this again and again.
By letting other people understand what they did and not to do the same mistake again, you'll need to spend less time on fixing rather than adding value, and also you'll pass some of your experience to your colleagues, as a true craftsmen should do.
Use composition, not inheritance
Recently I came across a bunch of code that needed some refactoring (apart from completion!). In particular, this class caught my attention.
[gist id="be59cdf4fc8ae0e47b0f"]
The first thing that should come to your mind (as it came to mine) is that, if we're not defining any additional behaviour, we shouldn't define a class, but simply use a syntax like Map<String, String> sessionContext
.
But, in addition to this, something more subtle is just around the corner.
For example, after having defined such a class, somebody could think of adding some functionality, like counting the number of elements put into the map. There's seemingly nothing wrong in doing:
[gist id="2808051a328e2344d91a"]
However, this won't lead to the expected behaviour. Why? In HashMap
, putAll()
will call multiple times put()
. Since both methods are overridden in the class, invoking putAll()
will add correctly the size of the Map
we passed as an input, but the put()
method will increase again the counter by 1 for every element in the map.
By extending HashMap
, we violated encapsulation - our class now depends on the implementation of HashMap
and any change in this class is directly reflected on our class. We clearly don't want this to happen, specially if our class depends on some implementation which we are not responsible for.
The solution to this problem is to use a different approach. Instead of extending the class, we should build a wrapper on top of it (or - in other terms - decorate it). The wrapper class will contain a reference to the dependency and instead of inheriting from that dependency, it will expose the same interface, adding functionality to the existing methods that will be then called on the contained object.
[gist id="43216d95475943a66f3a"]
Not only this will make our implementation less coupled to the dependency, but we can apply it not only to HashMap
, but to any other kind of Map
.
The problem with the situation described before is that inheritance should be used only when we're sure that the classes have a "is-a" relationship. By inheriting a class, we inherit all potential problems in that class - and specially if we're not responsible for that class, problems may rise. Unfortunately, Java API has got some problems regarding this aspect.
(note: although I used this real-world example for describing why we should use composition over inheritance, I still think that for the problem I supposed they were trying to solve with the original class using Map<String, String> sessionContext
would be better!)
Why you should learn another programming language
When I wanted to learn programming, I was lucky enough to get exposure to three different programming languages... well, if you can call "exposure" the kind of exposure a 10-years old kid can get! My first computer was BASIC based, and I played with C and Pascal for a while. Regrettably I have to admin that, until I've got to school, I've had no idea what a pointer was...
My university studies were more theoretical and less based on programming languages, although I had the luck to learn something about C++ by some projects and some personal studies. I discovered Java in my early 20s, and that became my main (and favourite, at that time) language. I started working on personal projects using Java, and all my jobs to date have been mainly Java based.
As I've got more experienced in Java, I've found myself often thinking in terms of programming constructs rather than abstractions that should be agnostic to the language. This is not a good thing - tying yourself to the language narrows the solutions you can think about. If you know only a programming language, or let's say only a paradigm, there are significant chances that you can implement the same solution with a simpler approach, in another language.
The limits of my language mean the limits of my world.
Ludwig Wittgenstein
The Pragmatic Programmer (a book I'm reading at the moment, and I really recommend it!) suggests you to learn at least one language per year. And, while you probably won't be able to pursue this goal at work (it's not a smart move to experiment a new language for a risky project, i.e.), learning this in your time will allow you not only to broaden your views, but also to expand your portfolio and then to be able to advise solutions using the language you've learnt. This is one of the most relevant asset you can offer as a software craftsman.
P.S. I'm currently learning Scala, as I think some exposure to a functional programming language (although Scala is not purely functional) will broaden my views. Probably I'll blog something about it in the future!