Unit Testing 101

Abhishek Gupta
WhatfixEngineeringBlog
7 min readOct 22, 2020

How often do you run into a late-night bug fix in the production only to realise that it turned out to be a silly code mistake which could have been avoided even before raising a pull request itself?

Or beat this — How often do you spend sleepless nights brooding over your code that had just hit the production where you have no idea if existing functionalities have remained intact as always?

All mature software engineers go through such phases in their career. However, nobody likes to remain glued to the dumb screen in the middle of the night, fixing a production bug. Including me!

As wise people say — prevention is better than cure, smart software engineers follow the same. They write plenty of extensive test cases only to sleep peacefully at night! I have always believed that Unit Testing is not Quality Engineers responsibility but lies with the developer.

There are several kinds of testing. Unit Testing is one of them.

What is Unit Testing

Unit testing is the most basic level of testing that should be done right at the development phase. It has to do with the methods of a class only and not with the whole project or with any other external interactions, be it another component or database, etc. Unit testing intends to validate whether a “unit” piece of work within a module is independently functioning as expected or not, given any variation of acceptable valid or invalid inputs. Ex: What if null is passed as an argument?

What is a “unit”?

A unit is an independently manageable and testable code that can be :

1. an individual method with a clearly defined responsibility to test

2. functionality exhibited by method(s) aimed to achieve a certain goal/output.

As mentioned above, unit testing should always be provisioned during the development cycle of a feature/component, not after a feature hits the production. Another way to drive development is by writing test cases with stubs in advance and then developing the feature around it. That is called Test Driven Development or TDD. For TDD to work effectively, the requirements should be well-known beforehand so that the test cases can be defined accordingly.

If it gets too hard to identify and test a unit then always break the unit in sub-units which are easier to manage. Lesser complicated the code, the easier it is to test.

How to Unit Test in Java

Java provides a JUnit library, which helps in writing test cases in isolation. Let’s see a sample structure of JUnit Test

import org.junit.Assert.*
import org.junit.Before
import org.junit.Test

public class SampleJUnitTest {

@Before
public void setup() {
// Setup the class dependencies here
// Which are required to run a successful test
}

@Test
public void testSomeMethod() {
// Call the required logic
// Assert the returned result
}

@After
public void tearDown(){
// De-initalize the resources, if any after each test run
}

}

All the above annotations used in above code snippet carry a special meaning in terms of execution:

  1. @Before — indicates the JVM to run the setup method before each test case
  2. @Test — Test case to test the “unit”.
  3. @After — Runs after each test case, primarily used to de-allocate resources after each test case.

JUnit5 comes with the added support of JUnit Jupiter and Vintage. That’s like adding a turbocharger to your basic car. Ex: Jupiter enables nesting of test cases, parallel executions, timeouts, etc.

Ref: https://junit.org/junit5/docs/current/user-guide/

Naming convention recommendation:

It’s always recommended to have appropriate naming conventions for variables, methods, classes, packages, projects, etc. So why should a test case be any different?

However, a verbose test case often expresses clearly what it is supposed to test, including what it is expecting as input/output for test case execution.

As an example, if I am testing login functionality, I usually follow these templates for naming a test case:

  1. testActionWhenInputExpectOutput — Eg. testLoginWhenNullUserNameExpectInvalidDetailsMessage. This test case simply (and clearly) specifies what it’s testing. If a null username is passed, “invalid details” message is expected.
  2. testActionWhenInputThrowException — Eg. testLoginWhenIncorrectCredentialsThrowUserAccessException. This test case will test the login flow for checking if the code under test (CUT) throws UserAccessException or not on providing incorrect credentials.

It’s plain simple English, eh?

Do not be conservative on the length of the test case name. If the test case name gets too long, it’s still okay. The idea is to have a name as verbose as possible.

Imagine this — there will be 100 or 1000 of test cases for any project, and it would become a tedious task to look into each failed test case to understand what it is doing. Hence, keep the name such that it is intuitive enough for others to read and derive the meaning out of it.

Interactions with external objects/components:

Till now, we have discussed to test a “unit” for each test cases. However, in real-world software, you wouldn’t find the “unit” operating in complete isolation. There are high chances that the “unit” will interact with objects of other classes. In essence, several “units” come together to collaborate and bring out the required behaviour of the system. Have a look at the below diagram

As established above, we should write unit tests for Class A and Class B separately. Logically, we should write unit tests of Class B before we write those for Class A as the methods of Class B are expected to be used outside its scope. Hence, all the “units” of Class B must be extensively tested of their expected behaviour and functionality.

When a unit of any other class (like Class A in our example) interact with a unit of Class B, we need not re-test the behaviour of Unit 2 while testing Unit 1 as it is understood that Class B works as expected. Hence, we mock the interaction between Unit 1 and Unit 2. Mocking allows unit 1 to proceed ahead with the execution without being involved in the unnecessary execution cycles by Unit 2.

We should only mock those interactions where we are 100% sure that the mocked unit will behave exactly likewise for the real-world application.

Mocking is incredibly handy when interaction across the units is processing intensive. E.g. if Unit 2 makes a call to DB to fetch details of a user and then filters them out based on certain criteria. All these processing of unit 2 will take some CPU cycles which don’t serve any purpose while testing unit 1. However, these interactions do serve great purposes during integration testing, which is outside the scope of this article.

Java provides a handful of good mocking libraries like Easymock, JMock, Mockito and PowerMock. Now-a-days, developers prefer to use Mockito and PowerMock due to their simplicity of usage and wide range of mocking options.

Did you know that you could also mock private and static methods using PowerMock?

Limitations of Unit Testing

No individual testing strategy is fool-proof. So is with the unit testing. Even though, having unit tests reduce the possibilities of bugs in the system exponentially, yet these are not 100% reliable. We have other forms of testing like integration testing, black box testing, UAT, etc to make the system more predictable and reliable. Let us visit some of the limitations of Unit testing:

  1. Since the scope of unit tests is limited to a class (or a group of class), we really cannot make out how the overall system performs.
  2. As unit tests are written with an intention of only to test the functionality of the code, these are generally not used for testing performance. For performance testing, whole system interaction comes to play. Issues like latency, multi-threading, etc are hard to evaluate using unit tests.
  3. Writing efficient unit tests is time taking. When code has to be refactored, associated unit tests also have to be modified.
  4. Efficiency and effectiveness of a unit test are inversely proportional to the complexity of the “unit” under test. More complex the actual code, the lesser effective will the unit test grow out to be.

Conclusion

From the above discussion, it’s clear that unit testing serves an important role in maintaining a healthy codebase. It is an assurance that the past written code doesn’t break with the new changes. In fact, in the absence of proper unit testing in place, there is no certain way of determining the correctness of code, and relying solely on manual testing of code is not always feasible. Unit testing works on the principle of fail fast, fail early, i.e. fail as quickly as possible during the development phase itself. The longer it takes the discovery of the failure, the higher the impact on fixing it.

The quality and quantity of unit tests determine the level of confidence in the system.

Besides all the arguments, having good unit tests (as well as integration tests) gives a new developer an extensive idea of the whole system. For many developers like me, unit and integration tests are play area for familiarizing ourselves with the codebase and its functionalities.

Unit tests are also widely used for determining the code coverage — an area which gives an added assurance to the developer. It’s a good practice to have coverage as close to 100% as possible. Well written unit tests (along with good coverage) make the major refactoring of code (especially legacy ones) a smooth process as there are multiple possibilities of missing out on a few functionalities leading to production bugs otherwise.

Code coverage can be checked at different stages e.g. development, testing, or deployment (by setting up CI/CD pipeline integrated with third party tools). However, for most organisations, developers have limited access to CI/CD pipelines (during deployment). Hence, it is often a best practice to have code coverage check done at the development phase itself(along with other stages). Almost all the IDEs can be equipped with plugins for testing code coverage. One such plugin is JaCoCo. With JaCoCo, you can even set the build goals.

After all, it’s not about always firefighting. It’s more about careful planning and execution!

This is a series on Unit Testing. We will dive into depth in subsequent articles.

Hope you enjoyed reading this article. Show your support by claps and comments.

--

--