How to write Junit test cases for Comparator code coverage

Viewed 130

I have a Sorting mechanism using Comparator interface like below:

    public class SortByEmployeeIdByDesc implements Comparator<Employee> {

    public int compare(Employee a, Employee b)
    {
        return b.getEmpId().compareTo(a.getEmpId());
    }

   }

Here is my Employee class:

    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class Employee {
      @Getter
      private String empId;

      private String empName;

      private Date createdTime;

      @Getter
      private Date lastUpdatedTime;
    }

I have written test cases like below for the above comparator:

    @ExtendWith(MockitoExtension.class)
    public void EmployeeSortTest{

    @Test
    public void sortEmpDescloyeeTest() {
    
    Employee emp1 = new Employee("abc", "test", "2020-10-10", "2010-10-10");
    Employee emp2 = new Employee("xyz", "test2", "2022-03-04", "2022-03-04");
    
    List<Employee> sortEmp = new ArrayList<>();
    sortEmp.add(emp1);
    sortEmp.add(emp2);
    
    Collections.sort(sortEmp, new SortByEmployeeIdByDesc());
    assertEquals(emp1, sortEmp.get(1));
}
}

The above test case is running successfully, but when I am checking in Sonar, it's not covering the code. Is there a better Junit test approach so that the coverage increases to at least 90%?

3 Answers

I've made some assumptions about language version and dependencies and run your code:

  • Java 8+ (language level 8)
  • JUnit 5 (Jupiter)

Your new Employee(...) constructors weren't working because your @AllArgsConstructor is expecting a Date object not a String.

Assuming you're using Java8+, you should use LocalDate instead of Date.

Code coverage yields the following results.

Employee.java: 50% Uncovered lines:

@Builder
@NoArgsConstructor
// omitted for brevity
@Getter
private LocalDate lastUpdatedTime;

SortByEmployeeIdByDesc: 100%

To cover the new lines, you should consider changing your test class as follows (for example):

@ExtendWith(MockitoExtension.class)
class SortByEmployeeIdByDescTest {

    private static final LocalDate MIN_CREATED = LocalDate.of(2020, 10, 10);
    private static final LocalDate MIN_LASTUPDATED = LocalDate.of(2020, 10, 10);

    @Test
    void sortEmpDescloyeeTest() {
        Employee emp1 = new Employee("abc", "test", MIN_CREATED, MIN_LASTUPDATED);
        Employee emp2 = new Employee("xyz", "test2", LocalDate.of(2022, 3, 4), LocalDate.of(2022, 3, 4));

        List<Employee> sortEmp = new ArrayList<>();
        sortEmp.add(emp1);
        sortEmp.add(emp2);

        sortEmp.sort(new SortByEmployeeIdByDesc());
        assertEquals(emp1, sortEmp.get(1));
    }

    @Test
    void noArgsConstructorTest() {
        Employee employee = new Employee();
        assertNull(employee.getEmpId());
        assertNull(employee.getLastUpdatedTime());
    }

    @Test
    void builderTest() {
        Employee expectedEmployee = new Employee("testId", "test", MIN_CREATED, MIN_LASTUPDATED);
        Employee employee = Employee.builder()
            .empId("testId")
            .empName("test")
            .createdTime(MIN_CREATED)
            .lastUpdatedTime(MIN_LASTUPDATED)
            .build();
        assertEquals(expectedEmployee.getEmpId(), employee.getEmpId());
        assertEquals(expectedEmployee.getLastUpdatedTime(), employee.getLastUpdatedTime());
    }
}

Because you've only declared @Getter on two of the fields, you can't test the others in the builderTest.

If you want to compare all the fields you could do the following:

  1. Add the @EqualsAndHashCode annotation on Employee -

     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     @EqualsAndHashCode // this annotation adds equals and hashCode methods
     public class Employee
     // omitted for brevity
    
  2. Change the builderTest code as follows -

     @Test
     void builderTest() {
         Employee expectedEmployee = new Employee("testId", "test", MIN_CREATED, MIN_LASTUPDATED);
         Employee employee = Employee.builder()
             .empId("testId")
             .empName("test")
             .createdTime(MIN_CREATED)
             .lastUpdatedTime(MIN_LASTUPDATED)
             .build();
         assertEquals(expectedEmployee, employee); // you now use equals
                                                   // instead of testing
                                                   // each field 
                                                   // individually
     }
    

Both approaches (with or without the @EqualsAndHashCode annotation) yield 100% code coverage. Bear in mind that unless you have logic in your getters and setters, covering them is usually considered trivial because you are essentially testing Java assignment, which one may assume to work.

To test a comparator, the first idea may be to add a few items to a list, sort the list and then ensure that the items have been sorted correctly.

This approach has a terrible downside though: sorting a list performs as few comparisons as possible, which is exactly the opposite of what you are trying to accomplish. Plus, depending on the sorting algorithm, you cannot be sure which combinations have been compared at all.

I once wrote a ComparatorTester that took a list of elements and then compared each element to each element. It also made sure that the comparator is reflexive, antisymmetrical and transitive.

When I googled for ComparatorTester, I found an implementation by Firebase that felt remarkably similar to my own implementation, as it uses the exact same idea. That's the approach you should follow.

In your trivial use case, you don't even need to write your own comparator class, that's a thing from before Java 8. Nowadays, sorting by a single property becomes a one-liner:

employees.sort(Comparator.comparing(Employee::getEmpId).reversed());

The Comparator.compare(T o1, T o2) javadoc states (emphasis added):

Returns: a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.

Therefore, at the minimum your test needs to cover the above three conditions:

class SortByEmployeeIdByDescTest {

    @Test
    void compareEmployees() {
        final List<Employee> employees = Stream.of("2", "1", "3", "2")
                .map(id -> Employee.builder().empId(id).build())
                .collect(Collectors.toList());

        Collections.sort(employees, new SortByEmployeeIdByDesc());

        assertThat(employees).extracting(Employee::getEmpId)
                .containsExactly("3", "2", "2", "1");
    }
}

I'm using AssertJ in this example and highly recommend using it in your projects.

In addition, your employee id is a String. So you need a test for when it's null. Then you will have the desired coverage.

Related