简体   繁体   中英

What's the best way to test a comparator that uses another comparator in Java?

From a TDD perspective, I've learned that when something behavioral breaks, only one test should fail - other failures are usually misleading. If this is true and I have two comparators and one of the comparators uses the other comparator, how do you test this? I was thinking of using a mock for the "sub-comparator," but how would you inject this mock when, for example, sorting a list using the "parent comparator?"

For example:

public class SomeParentComparator implements Comparator<SomeType> {

    private static final SomeSubComparator subComparator1 = new SubComparator();
    private static final SomeOtherSubComparator subComparator2 = new SomeOtherSubComparator();

    @Override
    public int compare(SomeType someType1, SomeType someType2) {
        return new CompareToBuilder()
                .append(someType1.foo, someType2.foo, subComparator1)
                .append(someType1.bar, someType2.bar, subComparator2)
                .toComparison();
    }
}

In the above, assume I've already tested the "sub-comparators" (SomeSubComparator and SomeOtherSubComparator). How do I test SomeParentComparator in this case without having a "true dependency" on the subcomparators (for example, mock the sub-comparators)? Really, this should somehow be a "workflow" unit test and just make sure the "sub-comparators" get called, right? How?

Your SomeParentComparator is very difficult to test independently only due to the fact that you are directly initializing subComparator1 and subComparator2 in the class object's instance variables.

I'd recommend the have setters and getters for these two fields and initialize them using setters or constructor.

Then you can use the setters to set your mocked subComaparators .

You can also create a mock data where subComparator comparisons doesn't matter. I can give some analogy, say You want to sort People objects with their first name and last name. You parent comparator sorts by first name and sub comparator works on last name. Then your mock data will be a list of People whose last name are all same.

Ideally all of your classes would have all dependencies injected rather than implicit (in your case, through private static fields). However there are certainly many times when removing all implicit dependencies will overly complicate your code. In that case you have two options for unit testing:

  1. Structure your unit test runner so that the tests for the dependent classes only run if the tests for the depended-on classes pass.

  2. Use something like Powermock to bypass encapsulation during unit testing and inject mocked dependencies. This will allow the tests for dependent classes to pass even if the depended-on classes are broken.

In the example case you gave I can't see any reason why you can't make the dependency explicit. There's no need for the fields to be static - given all objects of this class will behave in exactly the same way. So it would be better to have an explicit collection of the 'sub-comparators' and expect the caller to add them explicitly.

I think you have misconstrued the TDD advice.

You actually have two subcomparator classes that can be tested independently. Then you have the "parent" comparator that has instances of the subcomparator as hard-wired components. Just test them separately ... without any fancy mocking.

Sure, the correctness of the parent depends on the correctness of the subcomparators. But since the former and latter are inseparable it is both easier and more correct to treat the parent as a black box for the purpose of testing.

Thinking of this another way, @ShanuGupta's answer suggests that you should break open the abstraction of the parent comparator to allow mocking. Presumably you encapsulated the subcomparator instances for a good reason. Now you could create the parent constructor using DI ... but once again you are effectively breaking open the abstraction to so this.

Or another way. Suppose you break open the encapsulation. Now you have a parent class that (conceptually) ought to work for all possible subcomparator classes. (Because someone could change the code to use different comparators without changing the parent itself.) But that potentially means you have a more complicated set of behaviors to test.

In this case I wouldn't bother about mocking and I'd test the whole thing (all 3 comparators) as a single unit.

I imagine tests could look something like the following:

@Test
public void parent_with_smaller_foo_and_equal_bar_is_smaller() {
  var parentA = aParent().withFoo("A").withBar("C");
  var parentB = aParent().withFoo("B").withBar("C");

  assertThat(parentA).isLessThan(parentB);
}

@Test
public void parent_with_equal_foo_and_equal_bar_is_equal() {
  var parentA = aParent().withFoo("A").withBar("C");
  var parentB = aParent().withFoo("A").withBar("C");

  assertThat(parentA).isEqualByComparingTo(parentB);
}

and so on. If you'd write the above as:

(A,C)<(B,C)
(A,C)=(A,C)

then it looks like we'd need at least 3 more cases to test-drive all three comparators:

(B,C)>(A,C)
(A,B)<(A,C)
(A,C)>(A,B)

I would keep SubComparator and SomeOtherSubComparator as a package-private classes and treat them as implementation details of SomeParentComparator .

With as little as 5 test cases I don't consider finding the fail reason to be a real problem.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM