Unit Testing Best Practices

It is overwhelmingly easy to write bad unit tests that add very little value to a project while astronomically inflating the cost of code changes. This post will go through the JUnit best practices we must consider while writing the test cases.

In programming, “Unit testing is a method by which individual units of source code are tested to determine if they are fit for use.” This unit of code can vary in different scenarios.

For example, in procedural programming, a unit could be an entire module but is more commonly an individual function or procedure. In object-oriented programming, a unit is often an entire interface, such as a class, but it could be an individual method. Intuitively, we should view a unit as the smallest testable part of an application.

1. Unit Testing is ‘NOT’ for finding Integration Defects

Well, it’s important to understand the motive behind unit testing. Unit tests are ineffective for finding wide application bugs or detecting regression defects. Unit tests, by definition, examine each unit of our code separately. But when our application runs for real, all those units have to work together, and the whole is more complex and subtle than the sum of its independently-tested parts. Proving that components X and Y work independently doesn’t prove that they are compatible or configured correctly.

So, if you are trying to find regression bugs, it is far more effective to actually run the whole application in an embedded server as it will run in production, just like you naturally do when testing manually. If you automate this sort of testing in order to detect breakages when they happen in the future, it is called integration testing and typically uses different techniques and technologies than unit testing.

“Essentially, Unit testing should be seen as part of design process, as it is in TDD (Test Driven Development)”. This should be used to support the design process such that designer can identify each smallest module in the system and test it separately.

2. How to Write Good Unit Tests

2.1. Test only one code unit at a time

First of all and perhaps most important. When we try to test a unit of code, this unit can have multiple use cases. We should always test each use case in a separate test case. For example, if we are writing the test case for a function that is supposed to take two parameters and should return a value after doing some processing, then different use cases might be:

  • The first parameter can be null. It should throw an Invalid parameter exception.
  • The second parameter can be null. It should throw an Invalid parameter exception.
  • Both can be null. It should throw an Invalid parameter exception.
  • Finally, test the valid output of the function. It should return valid pre-determined output.

This helps when you do some code changes or do refactoring then to test that functionality has not broken. Running the test cases should be enough. Also, if you change any behavior, you need to change a single or least number of test cases.

2.2. Do not make unnecessary assertions

Remember that unit tests are a design specification of how a certain behavior should work, not a list of observations of everything the code happens to do.

Do not try to assert everything. Just focus on what you are testing; otherwise, you will have multiple test case failures for a single reason, which does not help achieve anything. A test should fail for a single reason.

2.3. Do not write dependent tests

Do not make a chain of unit test cases. It will prevent you from identifying the root cause of test case failures, and you will have to debug the code. Also, it creates dependency, which means if you have to change one test case, you need to make changes in multiple test cases unnecessarily.

Try to use @BeforeAll and @AfterAll methods to set up prerequisites for the test cases. If you need to do multiple things to support different test cases in @Before or @After, then consider creating a new Test class.

2.4. Mock out all external services and state

Otherwise, behavior in those external services overlaps multiple tests, and state data means that different unit tests can influence each other’s outcomes. You have definitely taken a wrong turn if you have to run your tests in a specific order, or if they only work when your database or network connection is active.

Also, this is important because you would not love to debug the test cases which are actually failing due to bugs in some external system.

By the way, sometimes your architecture might mean your code touches static variables during unit tests. Avoid this if you can, but if you can’t, at least ensure each test resets the relevant statics to a known state before running.

2.5. Do not test configurations

By definition, your configuration settings are not part of any unit of code (that’s why you extracted the setting out in some properties files). Even if you could write a unit test that inspects your configuration, then write only a single or two test cases for verifying that the configuration loading code is working and that’s all.

Testing all your configuration settings in each separate test case proves only one thing: “You know how to copy and paste.”

2.6. Name your unit tests clearly and consistently

Well, this is perhaps the most important point to remember and follow. You must name your test cases on what they actually do and test. Testcase naming convention which uses class names and method names for test cases name is never a good idea. Every time you change the method name or class name, you will end up updating a lot of test cases as well.

But, if your test case names are logical i.e. based on operations, then you will need almost no modification because most possibly application logic will remain the same.

E.g. Test case names should be like this:

  • testCreateEmployee_NullId_ShouldThrowException
  • testCreateEmployee_NegativeId_ShouldThrowException
  • testCreateEmployee_DuplicateId_ShouldThrowException
  • yestCreateEmployee_ValidId_ShouldPass

2.7. Write tests for methods that have the fewest dependencies first, and work your way up

This principle says that if you are testing the EmployeeManagement module, you should first test Create Employee module as it has the minimum dependency on external test cases. Once they are done, start writing Modify Employee test cases as they need some employees to be in the database.

To have some employees in the database, your create employee test cases that must pass before moving forward. In this way, if there is some error in employee creation logic, it will be detected much earlier.

2.8. All methods, regardless of visibility, should have appropriate unit tests

Well, this is controversial indeed. You need to look for the most critical portions of your code and test them without worrying if they are even private. These methods can have certain critical algorithms called from one or two classes, but they play an important part. You would like to be sure that they work as intended.

2.9. Aim for each unit test method to perform exactly one assertion

Even if this is not a thumb rule then also you should try to test only one thing in one test case. Do not test multiple things using assertions in a single test case. This way, if some test case fails, you know exactly what went wrong.

2.10. Create tests that target error cases

If some of your test cases, expect the exceptions to be thrown from the application, use the assertThrows(). try avoiding catching exceptions in the catch block and using the fail/ or asset method to conclude the test.

@Test
void testExpectedException() {

  ApplicationException thrown = Assertions.assertThrows(ApplicationException.class, () -> {
           //Code under test
  });

  Assertions.assertEquals("some message", exception.getMessage());
}

2.11. Use the most appropriate assertion methods

There will be many assert methods you can work with each test case. Use the most appropriate with proper reasoning and thought. They are there for a purpose. Honor them.

For example, for asserting two equal lists use this solution:

Assertions.assertThat(list1).containsExactlyInAnyOrderElementsOf(list2);

2.12. Put assertion parameters in the proper order

Assert methods usually take two parameters. One is the expected value and the second is the original value. Pass them in sequence as they are needed. This will help in correct message parsing if something goes wrong.

public static void assertEquals(T expected, T actual, String message)

2.13. Ensure that the test code is separated from the production code

Ensure that the test code is not deployed with the actual source code in your build script. It’s a wastage of resources.

2.14. Do not print anything out in unit tests

If you follow all the guidelines correctly, you will never need to add any print statements in your test cases. If you feel like having one, revisit your test case(s), you have done something wrong.

2.15. Do not use static members in a test class. If you have used then re-initialize it for each test case

We already have stated that each test case should be independent of the other, so there shall never be a need to have static data members. But, if you need any for the critical situation, remember to re-initialize to its initial value before executing each test case.

2.16. Do not write catch blocks that exist only to fail a test

If any method in the test code throws some exception, then do not write a catch block only to catch the exception and fail the test case. Such code clearly says that you do not understand how the code is working.

2.17. Do not rely on indirect testing

Do not assume that a particular test case tests another scenario also. This adds ambiguity. Instead, write another test case for each scenario.

2.18. Integrate tests with build script

It’s better if you can integrate your test cases with the build script so that they will get executed in your production environment automatically. This increases the reliability of the application as well as the test setup.

2.19. Do not skip unit tests

If some test cases are not valid now, remove them from your source code. Do not use @Ignore or -mvn.test.skip to skip their execution. Having invalid test cases in the source code will not help anyone.

2.20. Consider creating HTML Reports

Reports can be placed in a shared location or in a portal so that everyone knows the current state of the application.

3. Summary

Without a doubt, unit testing can significantly increase the quality of your project. Many scholars in our industry claim that any unit tests are better than none, but I disagree: a test suite can be a great asset, but bad ones can become an equally great burden that contributes little. It depends on the quality of those tests, which seems to be determined by how well its developers have understood the goals and principles of unit testing.

If you understand the above guidelines and will try to implement most of them in your next set of test cases, you will certainly feel the difference.

Happy Learning !!

Comments

Subscribe
Notify of
guest
11 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode