This section deals with the test configuration. Unit tests are not testing the system. In TDD, unit tests are written to obtain the following benefits:
You should configure your tests to follow the following principles:
The following section covers the test configuration.
Do not write unit tests that make HTTP requests, look up JNDI resources, access a database, call SOAP-based web services, or read from the filesystem. These actions are slow and unreliable, so they should not be considered as unit tests; rather, they are integration tests. You can mock out such external dependencies using Mockito. Chapter 4, Progressive Mockito, explains the mocking external dependencies.
Thread.sleep
is used in the production code to halt the current execution for some time so that the current execution can sync up with the system, such that the current thread waits for a resource used by another thread. Why do we need Thread.sleep
in a unit test? Unit tests are meant to get executed faster.
Thread.sleep
can be used to wait for a long running process (this is usually used to test concurrency), but what if the process takes time in a slow machine? The test will fail though the code is not broken, and this defeats the test reliability principle. Avoid using Thread.sleep
in unit tests; rather, simulate the long running process using a mock object.
Don't deliver unit tests to customers; they are not going to execute the tests. The test code should be separated from the production code. Keep them in their respective source directory tree with the same package naming structure. This will keep them separate during a build.
The following Eclipse screenshot shows the separate source folder structure. Source files are located under the src
folder, and the tests are placed under the test
source folder. Note that the Adder.java
and AdderTest.java
files are placed in the same package named com.packt.bestpractices.invalidinput
:
Static variables hold state. When you use a static variable in your test, it signifies that you want to save the state of something. So, you are creating inter-test dependency. If the execution order changes, the test will fail though the code is not broken, and this defeats the test reliability principle. Do not use static variables in unit tests to store global state.
Don't initialize the class to be tested as static and use the setUp
method (annotated with @Before
) to initialize objects. These will protect you from accidental modification problems. The following example demonstrates the accidental modification side effects.
The Employee
class stores employee names:
public class Employee { private String lastName; private String name; public Employee(String lastName , String name) { this.lastName = lastName; this.name = name; } public String getLastName() { return lastName; } public String getName() { return name; } }
The HRService
class has a generateUniqueIdFor(Employee emp)
method. It returns a unique employee ID based on the surname. Two employees with the surname Smith will have the IDs smith01
and smith02
, respectively. Consider the following code:
public class HRService { private Hashtable<String, Integer> employeeCountMap = new Hashtable<String, Integer>(); public String generateUniqueIdFor(Employee emp) { Integer count = employeeCountMap.get(emp.getLastName()); if (count == null) { count = 1; } else { count++; } employeeCountMap.put(emp.getLastName(), count); return emp.getLastName()+(count < 9 ? "0"+count:""+count); } }
The unit test class initializes the service as static. The service stores the input of the first test and fails the second test, as follows:
public class HRServiceTest { String familyName = "Smith"; static HRService service = new HRService(); @Test public void when_one_employee_RETURNS_familyName01() throws Exception { Employee johnSmith = new Employee(familyName, "John"); String id = service.generateUniqueIdFor(johnSmith); assertEquals(familyName + "01", id); } //This test will fail, to fix this problem remove the static modifier @Test public void when_many_employees_RETURNS_familyName_and_count() { Employee johnSmith = new Employee(familyName, "John"); Employee bobSmith = new Employee(familyName, "Bob"); String id = service.generateUniqueIdFor(johnSmith); id = service.generateUniqueIdFor(bobSmith); assertEquals(familyName + "02", id); } }
The following JUnit output shows the error details:
JUnit was designed to execute the tests in random order. It depends on the Java reflection API to execute the tests. So, the execution of one test should not depend on another. Suppose you are testing the database integration of EmployeeService
, where the createEmployee()
test creates a new Employee
, updateEmployee()
method and updates the new employee created in createEmployee()
, and deleteEmployee()
deletes the employee. So, we are dependent on the test execution order; if deleteEmployee()
or updateEmployee()
is executed before createEmployee()
, the test will fail as the employee is not created yet.
To fix this problem, just merge the tests into a single test named verifyEmployeePersistence()
.
So, don't believe in the test execution order; if you have to change one test case, then you need to make changes in multiple test cases unnecessarily.
The JUnit Theory
framework offers an abstract
class ParameterSupplier
for supplying test data for test cases. The ParameterSupplier
implementation can read from a filesystem, such as a CSV or an Excel file. However, it is not recommended that you read from the filesystem. This is because reading a file is an I/O (input/output) process, and it is unpredictable and slow. We don't want our tests to create a delay. Also, reading from a hardcoded file path may fail in different machines. Instead of reading from a file, create a test data supplier class and return the hardcoded data.
Sometimes the data setup for unit testing is monotonous and ugly. Often, we create a base test class, set up the data, and create subclasses to use the data. From subclasses, always invoke the setup of the super classes and teardown methods. The following example shows the fault of not invoking the super class.
We have EmployeeService
and EmployeeServiceImpl
to perform some business logic:
public interface EmployeeService { public void doSomething(Employee emp); }
The BaseEmployeeTest
class is an abstract
class, and it sets up the data for subclasses, as follows:
public abstract class BaseEmployeeTest { protected HashMap<String, Employee> employee ; @Before public void setUp() { employee = new HashMap<String, Employee>(); employee.put("1", new Employee("English", "Will")); employee.put("2", new Employee("Cushing", "Robert")); } }
The EmployeeServiceTest
class extends the BaseEmployeeTest
class and uses the employee
map, as follows:
public class EmployeeServiceTest extends BaseEmployeeTest { EmployeeService service; @Before public void setUp() { service = new EmployeeServiceImpl(); } @Test public void someTest() throws Exception { for(Employee emp:employee.values()) { service.doSomething(emp); } } }
The test execution fails with a NullPointerException
. The following is the JUnit output:
To fix this, call super.setUp()
from the setUp()
method. The following is the modified setUp()
method in EmployeeServiceTest
:
@Before
public void setUp() {
super.setUp();
service = new EmployeeServiceImpl();
}
Do not write test cases that affect the data of other test cases, for example, you are examining the JDBC API call using an in-memory HashMap
and a test case clears the map, or you are testing the database integration and a test case deletes the data from the database. It may affect the other test cases or external systems. When a test case removes data from a database, any application using the data can fail. It's important to roll back the changes in the final block and not just at the end of the test.
Be aware of internationalization while working with NumberFormat
, DateFormat
, DecimalFormat
, and TimeZones
. Unit tests can fail if they are run on a machine with a different locale.
The following example demonstrates the internationalization context.
Suppose you have a class that formats money. When you pass 100.99, it rounds up the amount to 101.00. The following formatter class uses NumberFormat
to add a currency symbol and format the amount:
class CurrencyFormatter{
public static String format(double amount) {
NumberFormat format =NumberFormat.getCurrencyInstance();
return format.format(amount);
}
}
The following JUnit test verifies the formatting:
public class LocaleTest { @Test public void currencyRoundsOff() throws Exception { assertEquals("$101.00", CurrencyFormatter.format(100.999)); } }
If you run this test in a different locale, the test will fail. We can simulate this by changing the locale and restoring back to the default locale, as follows:
public class LocaleTest { private Locale defaultLocale; @Before public void setUp() { defaultLocale = Locale.getDefault(); Locale.setDefault(Locale.GERMANY); } @After public void restore() { Locale.setDefault(defaultLocale); } @Test public void currencyRoundsOff() throws Exception { assertEquals("$101.00", CurrencyFormatter.format(100.999)); } }
Before test execution, the default locale value is stored to defaultLocale
, the default locale is changed to GERMANY
, and after test execution, the default locale is restored. The following is the JUnit execution failure output. In GERMANY
, the currency will be formatted to 101,00 € but our test expects $101.00:
You can change your code to always return the USD format, or you can change your test to run in the US locale by changing the default locale to US, and after test execution, restore it back to the default one. Similarly, be careful while working with date and decimal formatters.
If not used carefully, dates may act bizarrely in tests. Be careful when using hardcoded dates in unit tests. You are working with dates and checking business logic with a future date. On January 1, 2014, you set a future date as April 10, 2014. The test works fine till April 9 and starts failing thereafter.
Do not use hardcoded dates. Instead use Calendar
to get the current date and time and add MONTH
, DATE
, YEAR
, HOUR
, MINUTE
, or SECOND
to it to get a future date time. The following self explanatory code snippet demonstrates how to create a dynamic future date:
Calendar cal = Calendar.getInstance (); Date now = cal.getTime(); //Next month cal.add(Calendar.MONTH,1); Date futureMonth = cal.getTime(); //Adding two days cal.add(Calendar.DATE,2); Date futureDate = cal.getTime(); //Adding a year cal.add(Calendar.YEAR,1); Date futureYear = cal.getTime(); //Adding 6 hours cal.add(Calendar.HOUR,6); Date futureHour = cal.getTime(); //Adding 10 minutes cal.add(Calendar.MINUTE,10); Date futureMinutes = cal.getTime(); //Adding 19 minutes cal.add(Calendar.SECOND,19); Date futureSec = cal.getTime();
The following are the future dates when the program was run on April 16, 2014: