As testers, we want the features we release to the customers to have a high level of quality. It should be the same with the code we write for our automation tests. We want to have code that we are proud of, code that is reliable, easy to understand and easy to maintain. What? Why? How? Read on 💡
Clean Code: What is it, Why it’s important and How to write it?
- What is Clean Code?
- Why is Clean Code Important?
- How to Write Clean Code?
What does clean code look like? No matter what programming language we are writing our tests in, there are some common attributes that signal that our code is proper.
Good architecture makes things easy to find. When creating files, consider: The functionality they are testing; whether only the current test will use these bits of code, or will there be other tests that do; whether the code you are writing is test code or helper code. Based on these variables, you will be able to create the corresponding test files, helper files, and to group them in an adequate way.
If you are working on a Maven project, you get a hint from the project structure that you need to place certain bits of code here and others there. That is because you have a ‘main’ folder, for helper code, but also a ‘test’ folder, for everything test related.
One of the most underrated good practices regarding code is following naming conventions specific to the programming language you are using. But also, giving variables, classes, methods or functions a name that reflects their purpose. Don’t just name a variable ‘x’ or ‘var1’. Name it something meaningful.
Too often does a class or method try to do way too many things, instead of having a dedicated purpose. For example, in the same class, you might find helper code that processes some data read from the DB. But you could also have code for processing String variables and bringing them to a desired format. Such helper code is not test code, and could be beneficial for other tests. Therefore you should not write the helper code inside the test class. Instead, by separating concerns, you should have the test class holding only test code. The DB helper code should be stored inside DB processing classes, and String processing helper code stored inside dedicated classes.
Let’s face it, a lot of the code we need to write for our tests these days is not something that has not been written before. Maybe the code we need is already written inside our test framework. Or, in some cases, it might exist in some other external library that is used across the community. For such situations, instead of writing the same code again, we should reuse what already exists.
A test should comprise of as little code as possible. One of the things that help with this is the above mentioned ‘reusing existing code’. Another helpful aspect regarding this topic is the above mentioned ‘separation of concerns’. Both these principles will keep your tests focused on the testing part, hence the need for less code. Short tests mean that a test does not focus on testing the entire application at once. Instead, tests should focus on smaller parts, corresponding to user flows. The more steps a test is trying to perform, the greater are the chances for failures at random steps. This means such a long test will only rarely ever pass.
When you are writing code, you don’t only want to write the least code possible, but also the simplest one. Given two code variants that would solve the same problem, pick the simplest one. That is, if the performance of the two variants is similar. No need to do something over the top in complexity, just because this variant uses fancier libraries, for example, or fancier language constructs. Consider what happens if there is a failure in the tests and you need to debug your code.
Why would we care about clean code? There are quite a few good reasons, as you can see below.
Whenever somebody wants to take a look at some tests, they need to be able to figure out easily and fast what they are looking at. Especially if they are trying to fix some failing tests. Or, when you have new joiners to your team, they should be able to pick up fast and understand the existing automated tests. They can contribute early to the development process by running or updating existing tests. They could also follow the templates of existing tests in order to create new ones.
Tests sometimes require updates, either because they are failing randomly, or because the features they are testing have changed. When we need to make updates to the code, we need to easily figure out what needs to be updated, or where exactly in the code is the update needed. Having clean code makes it easy to navigate and find the place where the changes need to be applied. It also helps us understand what impact the changes required might have on other existing code in the framework. The simplicity of the code makes it easy and fast to perform the required updates.
Reusability of the existing code allows updates to be made in a single place. But, at the same, this allows all code that is using the one you updated, seamlessly receive the update.
When the code follows good coding practices, like simplicity, it is much easier to debug. Subsequently, it is easier and faster to find areas in the code that cause undesired behavior of the tests. Otherwise, if the code is too complex, or if there is too much of it, you will find it very difficult to figure out, first of all, where to add the breakpoints you will use in debugging. Secondly, adding too many breakpoints due to the complexity of the code will make the debugging process very long and, at one point very slow. Not surprisingly, the IDE you are using for debugging can only take so much.
Ok, ok. We want clean code. But how can we get there?
The first thing we need to do before writing any code, is to learn how to do that. And the best way to learn is from the official documentation of the programming language or framework we need to write code in. After all, who can tell us best how to use a tool, than the people who created it? The best thing about official documentation is that it helps you build up the knowledge, starting with the basics. You should always start with the basics. They are the foundation of the more advanced topics you can tackle later on.
Official documentation tells us how to use the language or framework. However it does not provide solutions to all the chunks of code we need to come up with as part of our daily work. Whenever you feel you need to create code for solving a particular situation, but the code you think of seems too complex, consult with your developers. They are right there, next to you, and have context on what you want to do, since they developed what you are testing. Especially if you and your developers are using the same programming languages, they can be the quickest resource to access, in order to help with your code. Having frequent discussions with developers will help you develop a code-oriented mindset similar to theirs. Soon enough you will start seeing that certain tasks that you initially planned to do manually can be done easier by means of coding.
Code reviews are another way of asking for help, but rather ‘after the fact’. That is because once you got to the code review phase, all the code you wanted to write is already written. Even so, feedback is highly recommended. The good thing about code review is that you can get feedback on two aspects. From testers, you get tips on what scenarios could be additionally covered by automation (apart from feedback on the actual code). From developers, you get mostly feedback on how you automate the scenarios, in order to make the code as efficient as possible.
Once the code review was performed, based on the feedback received, you can perform the refactoring of the code. Just because you spent days writing some code, if it can be improved, there is no problem in refactoring it. In fact, it is recommended. Refactoring early makes it way easier to do. When you refactor late, you need to make sure your changes do not affect any code that depends on the code you are changing. This can lead to having to: identify all the dependent code, run all the depending tests, realize that what needs to be refactored is affecting dependent code in an unpleasant way. For the latter case, you could end up having to perform way more tweaks than initially thought.
You could also perform refactoring before getting to the code review process, if you found a better way of writing the code.
When you have to do massive refactoring of existing code, chances are it won’t be easy. You might end up with several versions of the new code, but also with breaking a lot of the functionality of the dependent code. In order not to mess up the main branch, you should work on a dedicated branch. This allows you to commit the code you are working on as many times as you want, while still having the main branch clean. Yes, you should commit the code often. You should also sync the branch with the changes from the main branch frequently. Otherwise, by the time you want to merge the changes from the branch into the main one, you will have a lot of merge conflicts to solve at once. Frequent sync with the main branch allows easier merging of the two versions of the code.
Before starting to write the code, you should do analysis. First, you need to understand the requirements and the user flows you want to automate. Second, you need to map these flows to the code you want to write, and identify the pieces you need in the tests. By having some basic diagrams of the flows, you can identify common steps that can be translated to util or helper code. That code will be reused by other tests. You can also see what flows you should automate, and which ones are not worth automating. Visualizing what needs to be done, before you do it, will help you to do less refactoring, once the code is written.
If you are using IntelliJ, I highly recommend using its ‘Inspect code’ functionality. It provides static code analysis that you can perform either on the: current files, uncommitted files, or the entire project.
In my next post coming up soon, I will go over more clean code best practices, focusing on Java. Stay tuned! 🔜