8

Consider the following simple class hierarchy in Java

class Foo {
    protected void someMethod(Bar bar) {
        ...
    }

    protected void someOtherMethod(Baz baz) {
        ...
    }
}

class EnhancedFoo extends Foo {
    @Override
    protected void someMethod(Bar bar) {
        ...
    }
}

I now start writing JUnit unit tests for these two classes. Since the contracts for the method someMethod are same for both the classes, I need basically exactly the same test methods (concerning the someMethod method) for both the classes, which leads to code duplication. Doing this for a much richer class hierarchy with multiple overwritten methods, it just feels like inheritance is bad for testability.

Also, even though the method someOtherMethod is not overridden in ExtendedFoo, I need to include the pertaining tests for ExtendedFoo because that is still the contract and this extended class and unit tests should test this.

Is there some other way of organizing hierarchies in Java that is better for testability? Is there some JUnit construct that would help alleviate this problem?

ironstein
  • 416
  • 8
  • 23
  • What's the difference between what EnhancedFoo's someMethod has to do, and what Foo's someMethod has to do? There must be a difference, otherwise you wouldn't have written two versions of the method. – Dawood ibn Kareem Oct 24 '18 at 06:38
  • if you need exactly the same test, there was no need to overwrite the method – Stultuske Oct 24 '18 at 06:39
  • As far as I understand, unit tests assume classes and their methods to be black boxes and test their contracts. In such a case, the test is oblivious to whether the two methods are implemented in the same way or different. So, it doesn't matter whether the methods do the same thing, as long as they are bound to the contract. Isn't that right? – ironstein Oct 24 '18 at 06:41
  • @ironstein: what is the relation between `Foo` & `EnhancedFoo` ? If there is any, you are not showing that in your question. – Sabir Khan Oct 24 '18 at 06:50
  • @SabirKhan I am intentionally avoiding showing any implementation details, because, as I said earlier, the tests should not depend on the implementation details of the methods. Since the class `ExtendedFoo` extends from `Foo`, it has a responsibility to maintain the contract put forth by the class `Foo`. And to validate that this contract is in fact being maintained, we need to write the exact same tests. – ironstein Oct 24 '18 at 06:52
  • @ironstein: What I meant that - `extends Foo` was missing from `EnhancedFoo ` declaration. I edited it. – Sabir Khan Oct 24 '18 at 06:53
  • @SabirKhan Yes, I just saw it and I now understand the confusion! Thank you. – ironstein Oct 24 '18 at 06:54
  • I think there is a problem in understanding what the unit of the test is. And the unit shouldn't be a class, but a use-case requirement. A new requirement needs a new behavior which should be tested and of course, those tests are then different. So don't couple your tests with classes (classes are just an implementation detail). – ttulka Oct 24 '18 at 06:59
  • 1
    Inheritance is generally a way how to couple the components. Consider composition instead. You API contract should be specified as an interface. – ttulka Oct 24 '18 at 07:01

3 Answers3

3

One approach we used when we had a very similar scenario was to also reuse the est classes:

class FooTest {
    @Test
    public void testSomeMethodBar() {
        ...
    }

    @Test
    public void void someOtherMethodBaz(Baz baz) {
        ...
    }
}

And extend it for subclass tests:

class EnhancedFooTest extends FooTest {
    @Test
    public void testSomeMethodBar() {
        ...
    }
}

JUnit will run this specific overridden test method, and also the other default tests in FooTest. And that eliminates unnecessary duplication.

Where appropriate, some test classes are even declared abstract, and are extended by concrete test classes (tests for concrete classes).

ernest_k
  • 44,416
  • 5
  • 53
  • 99
  • Although this looks like a really neat solution (and I can't believe I did not think of this), I see a lot of articles (and a few stack overflow questions) that are aimed specifically at avoiding inheritance when writing Unit tests. For example, [this](https://www.petrikainulainen.net/programming/unit-testing/3-reasons-why-we-should-not-use-inheritance-in-our-tests/) one and [this](https://stackoverflow.com/questions/6715281/how-to-avoid-inheritance-in-junit-test-cases) one. – ironstein Oct 24 '18 at 07:00
3

As I mentioned in my comments I think the tests should be decoupled from the implementation and be "use-case-driven". It could look like that:

interface Foo {
    public void doSomething(...);
}

class DefaultFoo implements Foo { ... }
class EnhancedFoo extends DefaultFoo { ... }

class MyUseCaseTest {

   private Foo foo = new DefaultFoo(...); 

   @Test
   public void someRequirement() { ... }
}

class AnotherUseCaseTest {

   private Foo foo = new EnhancedFoo(...); 

   @Test
   public void differentRequirement() { ... }
}

The best would be to get rid of inheritance whatsoever, but it's a different topic...

ttulka
  • 10,309
  • 7
  • 41
  • 52
  • 1
    I was going over the item **Favor Composition over Inheritance** in Effective Java, and the use of Composition over Inheritance keeps making more and more sense to me. – ironstein Oct 24 '18 at 07:17
  • I was going over the OpenJDK sources and I think that [this](https://hg.openjdk.java.net/jdk/jdk/file/ef0fed0a3953/test/jdk/java/util/Map/ToArray.java) test serves as a perfect example to your approach of use case driven testing. The same tests are run for all the Classes that implement the Map interface, irrespective of how they are implemented. – ironstein Oct 25 '18 at 12:27
  • @ironstein I wouldn't say perfect because there is only one long test for all the requirements, but it definitely follows the idea of testing against an API and not implementation. – ttulka Oct 25 '18 at 14:46
0

Quoting from your question and comments ,

As far as I understand, unit tests assume classes and their methods to be black boxes and test their contracts.

And

Since the contracts for the method someMethod are same for both the classes, I need basically exactly the same test methods (concerning the someMethod method) for both the classes, which leads to code duplication.

Both are wrong assumptions in the context of generic unit test concepts and might be correct assumptions in the very narrow context of TDD. You haven't tagged your question for TDD so I am just guessing that and TDD is all about what is acceptable & not necessarily very robust from code perspective.

TDD never stops a developer to write more comprehensive unit tests that satisfy him/her and not only contract.

You have to understand that unit tests are developer tools to assure for method accuracy and it doesn't assume methods as black boxes - it is supposed to test contract as well as implementation details ( code coverage ) . A Developer shouldn't write unit tests blindly like for trivial methods ( like setter / getter methods ) .

When you go in detail for code coverage , you will find that you will have to write multiple test methods for a target method covering all the scenarios.

If implementation of below method is not changed in class - EnhancedFoo , why write unit tests for it? It should be assumed that parent class tests would be covering it.

protected void someMethod(Bar bar) {
    ...
}

You write unit tests for methods that you change or add and shouldn't be worried about inheritance hierarchy.

I am simply trying to emphasize the importance of word unit :)

Sabir Khan
  • 9,826
  • 7
  • 45
  • 98
  • 2
    I think code coverage is a wrong metric. You can have 100% code coverage without actually to test anything. Use-case coverage is much better for the product quality. – ttulka Oct 24 '18 at 07:14
  • 1
    I think there might be some disagreement as to how unit testing should be done. For example looking at [this](https://stackoverflow.com/a/1043013/5414031) answer to a different question, the author's suggestions directly contradicts yours. – ironstein Oct 24 '18 at 07:14
  • @ironstein: Yes and I don't agree with that answer for all practical purposes. To me , my unit tests have always been testing **my implementation details** which automatically tests contract too while vice versa is not true always. – Sabir Khan Oct 24 '18 at 07:17
  • @ttulka: I was simply pointing it without discussing about its merits and demerits. What you are talking in my opinion would be more of an integration test instead of developer unit test.Thats why I said **unit tests for formality and blindly are bad** , your intention should be good for - **You can have 100% code coverage without actually to test anything** – Sabir Khan Oct 24 '18 at 07:19
  • 1
    @SabirKhan but the discussion is what this all is about. Practices and principles are much more important then just techniques. Testing your implementation makes your tests very hard to maintain. Always when your refactor something, you have to fix your test. It's inpractical and leads to ignorance of broken tests or avoiding to write them whatsoever. The product is defined via its API, so the API is that matters and what should be tested. Integration tests are for testing of components communication and co-working in the system. – ttulka Oct 24 '18 at 07:24
  • Exactly - **Always when your refactor something, you have to fix your test** . if you are a developer who thinks about not fixing unit tests after refactoring , good luck with that :) – Sabir Khan Oct 24 '18 at 07:26
  • 2
    @SabirKhan "Always when your refactor something, you have to fix your test" is something we should avoid. Refactoring must not have an impact to your product functionality just from the definition. – ttulka Oct 24 '18 at 07:27
  • @ttulka : Not being personal but I am not that smart a developer who will rely on his refactoring ( esp. large scale ) without fixing unit tests :) My motto have always been simple and it has worked pretty good as far as defect reporting by QA team is concerned - fix unit tests after changing code. – Sabir Khan Oct 24 '18 at 07:31
  • Avoiding duplicate code at the cost of product robustness and correctness is hard to argue. Seems you guys have forgot the very purpose of tests is to break the product. – Sabir Khan Oct 24 '18 at 07:33