See JavaUnitBestPractices, AbstractTest and TestingPatterns
I have begun calling these "Contract Tests", because they describe the contract that all implementors or inheritors must respect. Violating those contracts violates the LiskovSubstitutionPrinciple, and we all know that's just bad.
I use Contract Tests very aggressively to support Isolation Testing (testing objects in total isolation from the implementation details of their collaborators). I tried to write a good example, but it wasn't good. I'll try again later. The expected results in the Contract Tests for interface X become the assumptions I use when testing class Y that uses interface X. This is especially useful in one general area: business logic and the database.
I generally introduce a Repository interface to hide the database. I test-drive the database-aware implementation, but pull up the general "push and pull data" tests up as Contract Tests for Repository. These Contract Tests now describe the assumptions I'm allowed to use when I introduce fake or mock Repository objects into business logic tests.
DaleEmery once wrote that when he uses only Isolation Tests he sees disagreements between what objects do and what their clients/users expect them to do. Good Contract Tests help me avoid this problem so much that I rarely use Integration Tests or end-to-end tests for myself any more. I let my Customer write them, but I generally don't care. -- JbRainsberger
An Abstract TestCase is a TestCase for an AbstractClass that ensures that concrete implementations of the abstract class behave as expected.
The Abstract TestCase will have abstract methods to enable it to obtain concrete subclasses of the Abstract class under test, to obtain appropriate arguments for tests and expected results.
JbRainsberger put it well when he said:
Contrived Java Example:
/** * A source of messages (serializable objects). * Implementations may be JMS queues, file systems, etc. */ public abstract class Source { /** * Receive a Message from the Source. * @param timeout length of time in ms to wait for a message * @return a message or null if the source timed out */ public abstract Serializable receive(long timeout) ; } public abstract class AbstractSourceTestCase extends TestCase { /** * Get the Source to test */ public abstract Source getSource() ; /** * Prepare and get a message expected from the Source. * e.g. put a message on to a JMS queue so that * a JMS Source will then produce it. */ public abstract Serializable prepareAndGetExpectedMessage() ; public void testMessageReceived() { Serializable expected = prepareAndGetExpectedMessage(); Serializable received = getSource().receive(1000); assertEquals(expected, received); } public void testNoMessageReceived() { Serializable received = getSource().receive(1000); assertTrue(received == null); } }OK, so the above example is a little contrived but is based on something I have written and found very useful. My 'real' Abstract TestCase has about 9 test methods which would have been replicated for all the tests for my implementations - lots of effort saved and it has caught a number of subtle bugs.
Given that an abstract class defines the behaviour of concrete implementations, an Abstract TestCase tests that behaviour and ensures that implementations behave as expected. It also helps developers build new implementations - the new testcase implementing the Abstract TestCase helps them to do it much more easily.
Discussions
ChanningWalton wrote:
(JbRainsberger) Real-life example: I wish IBM/VAJ would have been more careful. I was working with EJBs/Access Beans and wrote a test.
public void testFindOnlyOne() throws Exception { MyAccessBean finder = new MyAccessBean(); Enumeration e = finder.findByUniqueIndex();// Expect one row assertTrue(e.hasMoreElements()); MyAccessBean first = (MyAccessBean) e.nextElement(); assertTrue(!e.hasMoreElements());// Shouldn't be any more. try { e.nextElement(); fail("There's more?! You just said there wasn't!"); } catch (NoSuchElementException? success) {} }This test failed. Why? com.ibm.ivj.ejb.runtime.AccessBeanEnumeration does not respect the contract of java.util.Enumeration. When hasMoreElements() returns false, nextElement() returns null instead of throwing NoSuchElementException?. In short, their "enumeration" is not an Enumeration, even though they implement that interface.
An abstract test case enforcing the contract of java.util.Enumeration would have helped here.
CategoryTesting: TestingFramework for Java. CategoryJava