Two Critical Tips for Unit Testing in Java

Two Critical Tips for Unit Testing in Java

evelopers write unit tests to check their own code. Unit testing differs from integration testing, which confirms that components work well together, and acceptance testing, which confirms that an application does what the customer expects it to do. Unit tests are so named because they test a single unit of code. In the case of Java, a unit usually equates to a single class.

A unit test is fully automated, non-interactive, and binary?that is, it either succeeds or fails. So running your code and examining its output to see if it works is not a test. Neither is writing a little “test driver” that drives your code and allows you to check logs to see if it’s working correctly.

For years, unit testing languished in the “I know I should be doing it” category, but now it finally has become part of the Java developer’s professional toolkit. Being a guru-level java programmer is no longer good enough. Now you have to know how to properly unit test the code you write, which is a good thing because it leads to higher-quality code, higher productivity, and lower maintenance and evolution costs.

In this article, I will discuss two issues that often vex Java developers when they attempt to unit test their code: whether to design their systems in a manner that facilitates testing, and how to test non-deterministic code. An issue that comes up frequently is the relationship between unit testing and the design of the system being tested. So it’s probably best that I cover this first.

When you write unit tests, sometimes you feel compelled to change your code just to facilitate the test, usually when you need to test a private method or attribute. Doing so is a bad idea. If you ever feel tempted to make a private method public purely for testing purposes, don’t do it. Testing is meant to improve the quality of your code, not decrease it.

Having said that, sometimes designing your system in a way that makes testing easier is still necessary. If you need to add a design element to support testing, ensure that it also increases the general quality of the system as a whole. If the design element doesn’t, then you’re designing your system only to facilitate testing?which I must stress again is a bad idea.

For example, you may have a system that connects to a database. It may be advantageous to allow your system to connect to a test database as well as a development database. If you design it in a way that allows the database to be configured then you’re facilitating testing, but you’re also increasing the quality of the design because the design makes your system more flexible (the first real benefit is that it’ll be able to connect to the production database without changing the code!). This design decision benefits both the system and the tests and thus is a good decision.

Say you have a class that can be instantiated only through a factory method (e.g., “Gang of Four”). You may need to test an object of this class, but for some reason you can’t call the factory. It may require resources passed as parameters that you don’t have in your testing environment. Should you make the default constructor public just so your tests can instantiate the object and run the tests? No, of course not. That would reduce the quality of the system’s design, allowing anyone to access a constructor that should be private. In this scenario, the tester needs to work harder to provide the needed resources and instantiate the object through the factory (perhaps through the use of MockObjects).

Now that I’ve addressed this issue, let’s examine some solutions to common testing problems. The first problem is determining how to test non-deterministic code.Some code is non-deterministic. In other words, the detailed results of a method are influenced by more than just the code in the method. Probably the simplest example of this is the return value of the System.currentTimeMillis() method. The exact results depend not on the code but on the underlying hardware. It effectively returns a value based on the system clock?a value that probably will be different each time the code is run.

Another example could be testing the time it takes a message to travel from a Web server to a browser over the Internet. This concept has too many variables to even create a predictable model. The Internet at a fine level of granularity is non-deterministic?even chaotic.

However, just because something isn’t predictable at a fine level of granularity doesn’t mean that its overall behavior isn’t predictable. If you aggregate the fine grain results into a larger figure, say an arithmetic mean, then you could make some useful predictions about the expected values.

For instance, you could have a requirement that 90 percent of all Web-based transactions should complete within 1/100th of a second. Your technique here would be simply to run the test code many times (in a loop) and compare the transaction times with one second, keeping track of the number of passes and fails. If at the end of the test less than 90 percent of the transactions fail, then the test fails too.

A test uses the following code to confirm that the performance of a transaction is within acceptable bounds:

private int doPerformanceTest(int numberOfRuns, int    requiredTimeInMilliseconds) {  int passed = 0;  Fragment fragment = new Fragment();  for(int i = 0; i < numberOfRuns; i++) {    long startTime = System.currentTimeMillis();    fragment.doTransaction();    long endTime = System.currentTimeMillis();    long runTime = endTime - startTime;    if(runTime < requiredTimeInMilliseconds) passed++;  }  return passed;}

The test that uses this method is responsible for asserting the performance criteria that should be met:

public void testPerformanceStatistics () {  double percentageRequired = 90.0;  int numberOfRuns = 1000;  int passed = doPerformanceTest(numberOfRuns, 100);  double percentagePassed = passed / numberOfRuns;  assertTrue("percentagePassed = " + percentagePassed, percentagePassed >= percentageRequired);}

This statistical testing technique is quite useful for addressing a number of problems. It is a necessity for testing methods that have results based on dates, times, or random numbers. It's also invaluable when testing for performance and other non-deterministic properties of your code.Unit testing is an important tool for the modern developer, and Java has an abundance of libraries that can help with the development of your tests cases (particularly JUnit, which I will discuss in detail in an upcoming follow-up to this article). Becoming familiar with them all is important because a majority of your projects will require you to use them. Unit tests are fairly easy to write and have very rapid performance, so it's not going to take you long to run them.


About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist