In my previous post on the TestProject blog, I looked at some common good practices for having clean code in your automation project. Now, let’s focus on some Java test automation specifics!
Java Test Automation Good Practices
- Java naming conventions
- Optimize imports
- Variables
- Methods
- Try/catches
- Console output
- Commenting your code
- Cleanup Summary
Java naming conventions
The first thing we need to focus on is the naming conventions we should use. Whenever we want to create something in Java we need to give it a proper name. The name should reflect what the purpose of that item is. But also, we need to use the proper case for the name:
- classes should start in upper case letter. Every following word needs to start in upper case. Any other letter should be lower case.
- methods should always start with lower case letters. Every new word that follows should start in upper case. The rest of the letters should be lower case. The same goes for variables and parameters.
- constants should be all upper case, with words separated by underscore characters (namely: _).
- package names should be all lower case.
Following these naming conventions helps a great deal with the readability of the code.
Optimize imports
Your test classes will have quite a few lines of import code. When you initialize an Object in the test class or call a method from an external class, you will need to add the corresponding import to the import section. Try to be mindful of how you create the imports: avoid using the * import whenever possible. Instead of importing all the classes from an external package, try to import only the ones you need.
Keep in mind that, if you are using IntelliJ, once you import 5 classes from the same external package: instead of having 5 dedicated imports (one for each class), you will get a * import of the package where all the 5 classes can be found. This is fine, as in this case, it makes sense to have the * import, since you are not importing only one class from that package. Here, it makes more sense to have a single import, with *, instead of 5 individual imports, which means 5 lines of code.
And, whenever you find you no longer need an import, don’t forget to remove it from the test class. We want to have a tidy working space, so we need to look after the import section too.
Variables
Whenever working with variables, make sure you understand the scope of the variable and that you declare it accordingly. Don’t create a variable as a class field, if you only use it in one method. In this case, make the variable local to that method, and declare it inside the method. Or, if you are using the variable only inside a ‘for’ loop within a method, declare it inside the ‘for’ loop.
Before you create a new variable, consider how often you will use it in the code. If you only need to initialize it by giving it a value, then pass it as a parameter to some method call, without any other usage in the project, you should not create a variable at all. Instead, you should directly pass the value to the method that requires it. This will save endless numbers of lines of code, since such a situation can occur frequently in our tests.
Methods
As I mentioned in my previous post, reusing existing code is very practical. But, if we are the ones who write the code that will be reused, we need to make sure it’s adequate. Once you find you have duplicate code in your class, or in several classes, extract a new method out of it. That is, if the duplicate code was not already extracted by someone else. In case it was, just replace the duplicate code with a call to the existing method.
Whenever you extract a new method out of existing code, be so kind and replace the duplicates with the calls to the new method. This helps with keeping a clean project. Depending on the IDE you are using, when extracting the new method, you might even get this suggestion in the extraction screen. In this case, you can seamlessly, on the spot, perform the replacement. Hint: IntelliJ helps you with this.
Methods – should be more than 1 line of code
Please consider the number of lines of duplicate code you want to extract into a new method. Do not create a new method for 1 line of code. By doing this, instead of working on the target of ‘less code’, you are increasing the number of lines of code in the project. One new line for the method definition, one line for the code you extracted, one line for the closing brackets, and there you go. Three lines of code instead of one. Instead, when extracting the repeating code into a method, make sure it has several lines of code.
One of the frequent situations I see in Selenium test code is having a dedicated ‘click’ method for each WebElement. If, in the tests, there are 3 WebElements, there will be 3 ‘click’ methods: clickElement1, clickElement2, clickElement3. These methods each have 1 line of code, which represents just the ‘click()’ method call. So, each method code looks like:
public void clickElement1() { element1.click(); }
This is exactly one of the situations where, instead of creating a new method, we should just use the ‘click()’ method directly in the test. Otherwise, whatever class we have for storing the WebElements and their corresponding ‘click()’ methods will be huge.
Methods – not too much code
Once you decided to extract some code into a dedicated method, be mindful of how much code you throw into it. If the newly created method has 200 lines of code, that is a bit too much. In this case, try to extract additional methods out of bits of code from within this method. Try to group the code you extract into methods based on the functionality it is performing. This way, when you read the code, you will better see what are the groups of actions it performs. And that is because each new method you extract has a meaningful name, that reflects its purpose.
By extracting further methods out of initially extracted methods you will make the code shorter. You will make the newly created methods available to other tests. But you will also make the code more readable and easier to update, in case you need to.
Methods – number of parameters
Most methods you will use in your tests will have a signature that allows you to pass values to the method call. This is done through the parameters you specify when creating the method. Parameters are also something you need to be careful about. The number of parameters should not be too high. Around 5 is a good number. A bad number however is 20 or 30.
If you find yourself needing a large number of parameters, consider refactoring the method. Maybe you can break down the initial method into several ones, each taking as parameters some of the initial ones. Or, maybe some of the parameters should not be passed as parameters. Instead, maybe the value you need to pass can be computed inside the method. For example, maybe you need to pass the current date or time. This can be generated directly inside the method, where it is needed, and not passed as a parameter.
Also regarding parameters, when you call a method, and the value of a parameter needs processing, maybe do the processing in the method itself. This allows for shorter method calls and readability. For example, you want to pass a String, but you first need to convert it to lower case and remove all its empty characters and numbers. Instead of performing all of this processing in the method call, pass only the String value when calling the method. Then, inside the method, process the value by: converting it to lower case and removing all its empty characters and numbers.
Another aspect to look for when it comes to parameters is whether you are passing a constant as a parameter. In this case, rather than doing this, try to make the constant value local to the method, and not pass it at all to the method call. Or, if you already defined the constant somewhere else, like in a different class, just use the predefined constant in the method (without passing it as a parameter to the method call).
Try/catches
A fan favorite Java construct, and a frequent source of invalid test results is the ‘try\catch’ block. Nowadays it’s everywhere in our tests. But many times, we don’t use it properly. The ‘try’ and ‘catch’ block is made up of two branches: the try, and the catch, of course. We should address both of them each time we write them.
When we ‘try’ something, we expect for that something to occur, in which case we want to continue with the test run. But in tests, often we don’t consider what happens when that something did not occur, but instead, an Exception happened. Not handling the ‘catch’ branch will make a test pass, even in this case when we did not want to have an Exception thrown. Hence, the ‘success’ state of this test is wrong.
Let’s take a look at some use cases of the ‘try/catch’ block.
Example 1 – test passes only if the Exception is not thrown
Let’s look at the following code:
... try { ... codeToBeRun(); } catch (NoSuchElementException e) { fail("This test failed!"); } ...
The ‘try’ branch contains the code that needs to be run. In case the code does not throw any Exception in the ‘try’ branch, the code from the ‘catch’ branch will not execute. The rest of the code from the test method will execute.
If you configured the ‘catch’ branch to catch, for example, a NoSuchElementException:
- if a NoSuchElementException is thrown, the code from the ‘catch’ branch executes. The test will fail with an AssertionFailedError with the ‘This test failed!’ message.
- However, if the code throws a different Exception in the ‘try’ branch, for example, StaleElementReferenceException, the ‘catch’ branch code does not execute. Here, the StaleElementReferenceException will fail the test and there will be no ‘This test failed!’ message.
Suggestion: for this example, you should drop the ‘try/catch’ block entirely. The only reason why you would want a catch block here, is to show some more meaningful failure message than what the Exception failure shows. But honestly, you should be fine just seeing an Exception, to know that there is a test failure, and where to find it, based on the Exception’s stacktrace. The new variant of the code would only be:
... codeToBeRun(); ...
Example 2 – test passes only if the Exception is thrown
Let’s look at the following code:
... try { ... codeToBeRun(); fail("This test failed!"); } catch (NoSuchElementException e) { } ...
In this case, in the ‘try’ branch, if the ‘codeToBeRun()’ line of code throws a ‘NoSuchElementException’, nothing else from this branch runs. Instead, we will jump to the ‘catch’ branch. In this example, we have an empty ‘catch’ branch. No more code executes in the ‘try/catch’ block, and we will exit it successfully.
On the other hand, if the ‘codeToBeRun()’ line of code did not throw the ‘NoSuchElementException’, we want the test to fail. This is because whatever happened, it was not what we expect and wanted from the test. So, in order for us to fail the test, right after the line of code that was supposed to throw an Exception, we will call the ‘fail()’ method (either from JUnit or TestNG, depending on what you are using). This way we will mark the test a failed one.
Example 3 – test passes no matter what
Let’s look at a new piece of code:
... try { ... codeToBeRun(); } catch (NoSuchElementException e) { } ...
This test passes no matter if the NoSuchElementException is thrown or not. If the code from the ‘try’ branch does not throw any Exception, the ‘catch’ branch does not execute. If, however, the code from the ‘try’ branch throws the NoSuchElementException, the ‘catch’ branch does execute, but it’s empty. Therefore, no Exception is thrown (as it’s caught by the ‘catch’ branch) but no additional code executes either. In this case, the test passes.
Conclusion: We need to always consider what happens if the Exception from the ‘catch’ branch is not thrown, but also what happens if it is thrown. Also, don’t forget that if you have several lines of code in the ‘try’ branch, the first Exception thrown will exit the ‘try’ branch. So, if you expect a certain line of code to throw an Exception, limit the number of additional code you put into the same ‘try’ branch with that line, to avoid receiving a different Exception than the expected one.
Console output
Sometimes in your tests you feel the urge of writing ‘System.out.println’s. Sometimes a lot of them. But then, when you look at the console, it’s full of information that you don’t really need. I do like to print information to the console when I write tests, but only when they bring real value. For example, when the tests use randomly generated data. In case of a test failure, we need a way to easily figure out what happened. Performing a ‘System.out’ for these random values can help us in re-enacting what happened when the test failed. Otherwise, we will have no clue what data we passed through the system, and what exactly caused the failure.
Apart from this situation, try not to use too much console output. It’s ok to use it when you are trying to debug the code you are writing, but once you are done doing that, don’t forget to remove the ‘System.out’s.
Commenting your code
When we write our test automation code, we also use comments, to provide the readers information regarding what we are doing. But sometimes, we are giving too much unnecessary information. We tend to comment on every single line, method or class we write. But in reality, we should only comment those parts that seem odd: those lines of code that, when read by someone, will make them ask themselves why this code is written in such a way. Such code exists because, just to name a few reasons: the environment is unreliable and you had to write some workaround code; or you have a strange WebElement in a Selenium test that is a hassle to interact with. Try to remove any unnecessary comments, since those are lines of code too.
Cleanup Summary
Don’t forget that our goal when we write automation code is to write clean code. This involves tidying up any unused items, like imports, variables, or even methods. Also remove any commented code, if you will never use it again. Don’t just keep commented code, in case you will need it at some point. And, before you commit the code to the repository, make sure you take a good look at what you wrote 👀 If possible, run a static code analyzer on it, so you can polish it before you make it available to your teammates.
Share your experience in the comments below – How do you make sure your code is clean? 👩💻