There are lots of books about programming languages and frameworks - but only few resources that talk about "softer" topics like testing and collaboration. In larger projects, however, these topics can easily be make-or-break for your team's success!
We learned in countless situations within our own team how important these skills really are: very often when things went wrong, it had been due to a lack of testing and/or collaboration. Here are some things we've learned while growing Tower to serve more than 100,000 users.
Code is teamwork, not the work of any individual developer. This also means that none of the code you've written belongs to you – it belongs to the team! Looking at code in this way is helpful and liberating for a couple of reasons:
It doesn't have to go your way. After understanding that code always belongs to the team, it becomes obvious that we need to comply with coding standards and guidelines. Since it's not your code you can't just write it any way you fancy. It's a common code base, so common rules must be adhered.
This should also make you more tolerant when looking at your colleagues' code. You might instinctively think of all the ways you would have done it differently. But again: it doesn't have to go your way, but your team's way. This can help a lot in making you more open-minded towards other solutions and approaches.
Everything is your problem. When you spot a potential problem in the code, it doesn't really matter if it was your fault for not. Since all of the project's code belongs to the team – and you are part of this team – weak code from any source becomes your problem.
Contrast this to an atmosphere where people are detached from their teams and their shared code: "this is not my problem" is an attitude that will never produce great products.
You. You. You. This is a simple reminder of our responsibilities as software developers: You are responsible for writing code. Then, you are also responsible for making it shippable or production ready. And of course, you are responsible for fixing any problems with it.
In our own team, where we don't have a dedicated quality assurance team, this reminded us that developers are not only responsible for writing down lines of code – but also for the quality of that code. This responsibility is nothing that you can duck out of or hand over to someone else.
Code reviews have a huge potential to lift your development team to new heights.
Most obviously, discussing code in a review session can lead to a better solution than the original developer had had on her own. Two minds working together on the same problem will always have the chance to produce higher quality code.
But other potential benefits of code reviews might be even more interesting. Shared ownership, for example, is crucial to reduce knowledge silos: whenever you find that only a single developer in your team knows a certain piece of code, you are at risk. Whenever this developer goes on holidays, gets sick, or maybe even leaves the company you'll be in trouble.
But even if none of these situations occur: the fact that your team doesn't seem to have a truly shared code base is already telling. Have a look at the previous section and you'll understand why.
Additionally, code reviews can solve the hairy problem of knowledge exchange. If, like most teams, you have a mixture of junior and senior developers, you will certainly want those junior developers to benefit from your experts' experience and knowledge. Code reviews are an excellent way to pass on knowledge in situations like these.
And to make the process worthwhile for both sides (juniors and seniors), make sure your senior developers understand that both parties – the student and the teacher – will learn something on the way!
Since there is no such thing as a perfect solution, you could argue that everything can be improved. And you're probably right. But the interesting question is not if something can be improved – but rather if it's worthwhile to improve a certain part in your application.
We know where our own product, Tower, has its weak spots. And if time was infinite, we would probably polish and improve every single bit. But with time being a scarce resource for any team you will have to set priorities. If you don't, your product will not take the direction you want it to take - or it might very well take no direction at all.
Being able to responsibly ponder if or if not to improve something really is an art. It takes years of experience and, in the best case, even conscious training. Saying no to a lot of things is a prerequisite for achieving anything of worth.
True in life, and true in software development.
When reading other people's code it's all too easy to assume that you would have done it better. On first encounter, this or that line might seem unnecessarily complex or even superfluous. And you might of course be right. But it's very hard to say because of one simple fact: you didn't actually write the code.
From our own experience, we know that even the simplest problem can really contain a surprising and infinite amount of complexity. Only when actually working on the problem – when picking it apart, thinking about possible solutions, side effects, etc. – will one be able to understand its scope.
Therefore, when coming across a piece of code that makes you suspicious, it's a great ground rule to assume a good amount of hidden complexity. From this point of view you can either dive deep into the code and try to understand it and the underlying problem – or you simply get into a conversation with your teammate(s) and ask some humble questions.
Unit tests are sometimes regarded as the icing on the cake. More appropriately, however, they would be better described as the cake's base.
In smaller projects, you might get away without writing unit tests. In any project with just a slight amount of complexity, however, you will not. And, just as a side note, it's remarkable to see how many projects started simple and very soon became complex. So please don't act surprised.
There are indeed countless reasons why unit testing is essential for us:
- Making big changes, safely and quickly. In every larger project, you will have to perform refactorings at some point. But refactoring code without a decent amount of unit tests in place is a very frustrating experience: While fixing something you will often and inevitably break something else. Unit tests let you know when this happens.
- Releasing to the public, confidently. If your application is shipped to paying customers, not having a solid unit test coverage should make you shiver every time you make a release.
- Better code through tests. Writing good tests is one of the best preconditions to have good code in the end. This is because tests help you understand the problem you're trying to solve and create a simple, modular architecture.
- Up-to-date documentation. In an ideal world, every important part of an application would be nicely documented. We would have wonderful, clean, and beautifully formatted documents that are a joy to read. And most importantly, we would update the documentation every time something in the code changes. Well... this is of course a fairytale. But having up to date documentation in general does not have to be a fairytale: unit tests can serve this purpose. Although probably not as pleasant to read as dedicated documentation, they are (almost by definition) always up to date. This makes unit tests a very valuable and reliable source of documentation.
Good test quality and good test coverage of important code parts are mandatory for larger projects. It made us much more confident that we would always be able to deliver Tower with a high quality and that we would always be able to make changes (big and small) safely and effectively.
At first glance, using a test-driven approach would seem to be time-consuming and slow down your progress. And indeed, writing good unit tests takes time.
But over time, you will find that unit testing will in fact make you faster! For one thing, after thinking about your code design while writing a test, you will need less time to write the actual implementation code. You will have done a lot of the heavy lifting already by writing the tests.
The other benefit that will make you a lot more effective in the long run is stability: the likelihood of breaking existing code when making changes is significantly reduced through tests. You will save countless hours of bug hunting that you can now invest in something more useful.
Bug fixing should be inseparably connected with writing tests. Of course you could just track that bloody animal down and simply fix the code. However, I guess that a lot of you are painfully familiar with the term "regression": the annoying re-emergence of a bug that you were so confident to have fixed.
If you want to make sure (and you should!) that a bug is fixed for good, there is only on proven way - by writing a test to safeguard this piece of code.
While producing a software product that is in daily use by almost 100,000 people, we learned a lot about errors. For one thing, we learned that they are an inevitable part of the business; especially with the diverse and complex system setups that Tower has to deal with, you cannot rule out and safeguard everything. But for another thing, we also learned where errors mostly occured.
In Tower (and most likely in a lot of other larger applications), most errors tend to occur in low level components. The good thing about this is that such components lend themselves very well to unit testing. In this area, a test-driven approach offers a huge chance to improve your application's overall stability and quality with a reasonable investment – by thoroughly testing low-level components.
In general, having a large amount of your code covered by unit tests is a good thing. However, if you ever hear a software developer brag about his "100% test coverage", you should be wary. Testing every single line of your application code might sound like something to strive for - but it really isn't!
On the one hand, this would of course afford an infinite amount of time. Much more importantly, however, there is in fact something like writing too many tests! At the latest, you will learn this when making a bigger refectoring: not only your code, but also your tests have to be maintained. When your code changes, your tests need to change, too.
The crucial criteria is value. Does the test bring you enough value to justify writing it? Typically, tests are of low value if they test only very simple logic. In these cases, the burden of having to maintain the test outweighs the little value that it brings.
Writing tests efficiently, then, means preferring fewer high-value tests over many low value tests.
Manually testing a certain scenario in your application can really take a huge amount of time: you have to start the app, navigate your way to a certain view, and perform various additional actions to reproduce a certain scenario.
Writing a unit test for a scenario will indeed afford some time initially. But very quickly, probably already after having to test this only a couple of times, running the test will be much faster than starting the whole application and testing your way through it manually.
Besides being faster, however, the test will also provide better results in terms of quality and stability. In most cases, the initial effort is worth the long time benefits.
If you've read this far, I hope you agree with me: becoming a great software developer takes more than just writing code. From testing to documentation and from ownership to communication, there's a lot to learn if you want to master the art of programming.
Be sure to join our free newsletter for more insights and advice on this exciting journey!