17

I have a skeletal implementation, as in Item 18 from Effective Java (extended discussion here). It is an abstract class that provides 2 public methods methodA() and methodB() that call subclasses methods to "fill the gaps" that I can't define in an abstracted manner.

I developed it first by creating a concrete class and writing unit tests for it. When the second class came, I was able to extract common behavior, implement the "missing gaps" in the second class and it was ready (of course, the unit tests were created for the second subclass).

Time went by and now I have 4 subclasses, each implementing 3 short protected methods that are very specific to their concrete implementation, while the skeletal implementation does all the generic work.

My problem is that when I create a new implementation, I write the tests all over again:

  • Does the subclass calls a given required method?
  • Does a given method has a given annotation?
  • Do I get the expected results from the method A?
  • Do I get the expected results from the method B?

While see advantages with this approach:

  • It documents the subclass requirements through tests
  • It may fail fast in case of a problematic refactoring
  • Test the subclass as a whole and through their public API ("does methodA() work?" and not "does this protected method work?")

The problem I have is that the tests for a new subclass are basically a no-brainer: - The tests are all the same in all subclasses, they just change the return type of the methods (the skeleton implementation is generic) and the properties I check on the assert part of the test. - Usually the subclasses have no tests that are specific for them

I like the way the tests focus on the result of the subclass itself and protects it from refactoring, but the fact that implementing the test became just "manual work" makes me think I am doing something wrong.

Is this issue common when testing class hierarchy? How can I avoid that?

ps: I thought about testing the skeleton class, but it also seems strange creating a mock of an abstract class to be able to test it. And I think that any refactoring on the abstract class that changes behavior that is not expected by the subclass would not be noticed as fast. My guts tell me that testing the subclass "as a whole" is the preferred approach here, but please feel free to tell me I am wrong :)

ps2: I've googled and found many questions about the subject. One of them is this one which has a great answer from Nigel Thorne, my case would be his "number 1." That would be great, but I can't refactor at this point, so I would have to live with this problem. If I could refactor, I would have each subclass as a strategy. That would be OK, but I would still test the "main class" and test the strategies, but I would not notice any refactoring that breaks the integration between main class and strategies.

ps3: I've found some answers saying that "it is acceptable" to test the abstract class. I agree that this is acceptable, but I would like to know which is the preferred approach in this case (I am still starting with unit tests)

4
  • 2
    Write an abstract test class and have your concrete tests inherit from it, mirroring the structure of the business code. Tests are supposed to test the behaviour and not the implementation of a unit, but that doesn't mean you can't use your insider knowledge about the system to achieve that goal more efficiently. Oct 28, 2015 at 11:29
  • I read somewhere that one shouldn't use inheritance on tests, I am looking for the source to read it carefully
    – JSBach
    Oct 28, 2015 at 11:50
  • 4
    @KilianFoth are you sure about this advice? This sounds like a recipe for brittle unit tests, highly sensitive to design changes and therefore not very maintainable. Oct 28, 2015 at 12:43
  • Your gut is right, it's best to avoid unit testing abstract classes directly: enterprisecraftsmanship.com/posts/…
    – Vladimir
    Jun 25, 2020 at 11:51

2 Answers 2

10

I can't see how you'd write tests for an abstract base class; you can't instantiate it, so you cannot have an instance of it to test. You could create a "dummy", concrete subclass just for the purposes of testing it - not sure how much use that would be. YMMV.

Every time you create a new implementation (class), then you should be writing tests that exercise that new implementation. Since parts of the functionality (your "gaps") are implemented within each subclass, this is absolutely necessary; the output of each of the inherited methods may (and probably should) be different for each new implementation.

1
  • Yes, they are different (the types and content are different). Technically speaking, I would be able to mock this class or have a simple implementation only for testing purposes with empty implementations. I do not like this approach. But the fact that I do not "need" to think to have passing tests in new implementations makes me feel weird and also makes me question about how confident I am in this test (of course, if I change the base class on purpose, the tests fail, which is a proof that I can trust them)
    – JSBach
    Oct 28, 2015 at 12:07
6

Typically you would create a base class to enforce an interface. You want to be testing that interface, not the details below. Therefore, you should create tests that instantiate each class individually but only test through the base class's methods.

The advantages of this method is that because you tested the interface, you can now refactor underneath the interface. You may find that code in a child class should be in the parent, or vice versa. Now your tests will prove that you didn't break behavior when you move that code.

In general, you should test code how it's intended to be used. That means avoiding testing private methods, and often means that your unit under test may be a bit larger than you originally thought - perhaps a group of classes rather than a single class. Your goal should be to isolate changes so that you rarely have to change a test when you refactor in a given scope.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.