Patrick Collins
Patrick Collins

Reputation: 10574

Should my unit tests for derived classes inherit from my test for the superclass?

I have a unit test (JUnit 4) with some pretty complex setup:

public class HDF5CompoundDSCutoffCachingBridgeTest {
    protected final String TEST_PATH = "test-out.h5";
    protected final DatasetName TEST_DS = new DatasetName("group", "foo");
    protected WritableDataPoint testPoint1;
    protected WritableDataPoint emptyPoint;
    protected WritableDataPoint[] emptyPoints;
    protected WritableDataPoint[] fullPoints;
    protected IHDF5Writer writer;
    protected HDF5CompoundDSBridgeConfig config;


    protected HDF5CompoundDSCachingBridge<WritableDataPoint> dtBridge;

    @Before
    public void setUp() throws Exception {
        try {
            File file = new File(TEST_PATH);

            testPoint1 = new WritableDataPoint(new long[][]{{1, 2}}, new long[][]{{3, 4}}, 6, 10l);
            emptyPoint = new WritableDataPoint(new long[][]{}, new long[][]{}, 0, 0l);

            emptyPoints = new WritableDataPoint[]{emptyPoint, emptyPoint, emptyPoint, emptyPoint, emptyPoint,};
            fullPoints = new WritableDataPoint[]{testPoint1, testPoint1, testPoint1, testPoint1, testPoint1,};


            config = new HDF5CompoundDSBridgeConfig(HDF5StorageLayout.CHUNKED,
                                                    HDF5GenericStorageFeatures.MAX_DEFLATION_LEVEL,
                                                    5);

            writer = HDF5Writer.getWriter(file);
            HDF5CompoundDSBridgeBuilder<WritableDataPoint> dtBuilder =
                    new HDF5CompoundDSBridgeBuilder<>(writer, config);
            dtBuilder.setChunkSize(5);
            dtBuilder.setStartSize(5);
            dtBuilder.setTypeFromInferred(WritableDataPoint.class);
            dtBuilder.setCutoff(true);

            buildBridge(dtBuilder);

        } catch (StackOverflowError e) {
            // other stuff
        }

    }

    protected void buildBridge(HDF5CompoundDSBridgeBuilder<WritableDataPoint> dtBuilder) throws Exception {
        dtBridge = dtBuilder.buildCaching(TEST_DS);
    }

    // a bunch of tests follow
}

Now, I've subclassed the class that's under test here. It still needs the complex setup in the original test. It's behavior is pretty much the same -- it needs to do all of the things that the parent class does, plus it passes slightly different information to dtBuilder.

I could just copy-paste the configuration code over. This does appeal to me a little bit: these are really two different objects under test, and it's possible that in the future their configuration needs will change. If they have completely separate setup code, then I can make changes to the way either object works without worrying about breaking tests unnecessarily. But that seems wrong.

What I've done instead is inherit from the class for the superclass:

public class HDF5CompoundDSAsyncBridgeTest extends HDF5CompoundDSCutoffCachingBridgeTest {
    protected HDF5CompoundDSAsyncBridge<WritableDataPoint> dtBridge;


    @Override
    protected void buildBridge(HDF5CompoundDSBridgeBuilder<WritableDataPoint> dtBuilder) throws Exception {
        dtBuilder.setAsync(true);
        dtBridge = dtBuilder.buildAsync(TEST_DS);
        assertNotNull(dtBridge);
    }
    // more tests go here
}

I like this because I get to verify that my new class still passes all of the tests its parent class did, but there seems to be something wrong about having unit tests inheriting from each other.

What's the right move here? I've read posts like this: What does “DAMP not DRY” mean when talking about unit tests? that suggest that inheriting for unit tests is a bad idea, but it seems wrong to copy + paste whole chunks of code, ever.

Upvotes: 2

Views: 742

Answers (1)

Dave Newton
Dave Newton

Reputation: 160181

IMO there's nothing intrinsically wrong with tests living in a hierarchy, but it's not the only option, and there are a variety of arguments either way. Tests do tend to repeat more code, but for me, identical setup is too prone to errors, fat-fingering, etc. to rely on copy-and-paste, particularly if that setup changes often enough that it's likely you'd screw up one of the subclass's setup.

All that said, why not just call the superclass's setup method(s) from the subclass, overriding and/or extending behavior when necessary? It's not like the subclass lives in isolation.

If there's some reason that doesn't work, or there's a lot of subclass customization needed, consider wrapping up those options/values, datasinks, etc. in a value object.

Then each test class may use the same @Before functionality, but it would look more like:

protected TestValues testValues;

@Before
public void setUp() throws Exception {
    testValues = new TestSetupHelper();
}

If things need to be parameterized you can add stuff to TestSetupHelper's ctor etc.

Upvotes: 2

Related Questions