Configuring tests

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:

  • They drive your design. You write a test, add code to fix the test, refactor code with confidence, and apply the design. This results in a simple, clean, maintainable, loosely coupled, and cohesive design. You write code to satisfy a failing test, so it limits the code you write to only what is needed.
  • The tests provide fast, automated regression for refactoring and enhancing the code.

You should configure your tests to follow the following principles:

  • Unit tests should be executed extremely fast so that they can provide quick feedback. Would you withdraw money from an ATM that takes 10 minutes to dispense money?
  • Tests should be reliable. Tests should fail if the production code is broken. Your tests will be considered unreliable in situations where you break the production logic but the tests pass, or you don't touch the production code but still your tests fail.

The following section covers the test configuration.

Running in-memory tests

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.

Staying away from Thread.sleep

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.

Keeping unit tests away from the production code

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:

Keeping unit tests away from the production code

Avoiding static variables

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:

Avoiding static variables

Assuming the test execution order

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.

Loading data from files

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.

Invoking super.setUp() and super.tearDown()

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:

Invoking super.setUp() and super.tearDown()

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();
  }

Staying away from side effects

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.

Working with locales

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:

Working with locales

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.

Working with dates

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:

Working with dates
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset