Writing a single unit test for multiple implementations of an interface
Based on the anwser of @dasblinkenlight and this anwser I came up with an implementation for my use case that I'd like to share.
I use the ServiceProviderPattern (difference API and SPI) for classes that implement the interface IImporterService
. If a new implementation of the interface is developed, only a configuration file in META-INF/services/ needs to be altered to register the implementation.
The file in META-INF/services/ is named after the fully qualified class name of the service interface (IImporterService
), e.g.
de.myapp.importer.IImporterService
This file contains a list of casses that implement IImporterService
, e.g.
de.myapp.importer.impl.OfficeOpenXMLImporter
The factory class ImporterFactory
provides clients with concrete implementations of the interface.
The ImporterFactory
returns a list of all implementations of the interface, registered via the ServiceProviderPattern. The setUp()
method ensures that a new instance is used for each test case.
@RunWith(Parameterized.class)
public class IImporterServiceTest {
public IImporterService service;
public IImporterServiceTest(IImporterService service) {
this.service = service;
}
@Parameters
public static List<IImporterService> instancesToTest() {
return ImporterFactory.INSTANCE.getImplementations();
}
@Before
public void setUp() throws Exception {
this.service = this.service.getClass().newInstance();
}
@Test
public void testRead() {
}
}
The ImporterFactory.INSTANCE.getImplementations()
method looks like the following:
public List<IImporterService> getImplementations() {
return (List<IImporterService>) GenericServiceLoader.INSTANCE.locateAll(IImporterService.class);
}
I know this is old, but I learned to do this in a slightly different variation which works nicely wherein you can apply the @Parameter
to a field member to inject the values.
It's just a little cleaner in my opinion.
@RunWith(Parameterized.class)
public class MyTest{
private ThingToTest subject;
@Parameter
public Class clazz;
@Parameters(name = "{index}: Impl Class: {0}")
public static Collection classes(){
List<Object[]> implementations = new ArrayList<>();
implementations.add(new Object[]{ImplementationOne.class});
implementations.add(new Object[]{ImplementationTwo.class});
return implementations;
}
@Before
public void setUp() throws Exception {
subject = (ThingToTest) clazz.getConstructor().newInstance();
}
With JUnit 4.0+ you can use parameterized tests:
- Add
@RunWith(value = Parameterized.class)
annotation to your test fixture - Create a
public static
method returningCollection
, annotate it with@Parameters
, and putSinglyLinkedList.class
,DoublyLinkedList.class
,CircularList.class
, etc. into that collection - Add a constructor to your test fixture that takes
Class
:public MyListTest(Class cl)
, and store theClass
in an instance variablelistClass
- In the
setUp
method or@Before
, useList testList = (List)listClass.newInstance();
With the above setup in place, the parameterized runner will make a new instance of your test fixture MyListTest
for each subclass that you provide in the @Parameters
method, letting you exercise the same test logic for every subclass that you need to test.
I'd probably avoid JUnit's parameterized tests (which IMHO are pretty clumsily implemented), and just make an abstract List
test class which could be inherited by tests implementations:
public abstract class ListTestBase<T extends List> {
private T instance;
protected abstract T createInstance();
@Before
public void setUp() {
instance = createInstance();
}
@Test
public void testOneThing(){ /* ... */ }
@Test
public void testAnotherThing(){ /* ... */ }
}
The different implementations then get their own concrete classes:
class SinglyLinkedListTest extends ListTestBase<SinglyLinkedList> {
@Override
protected SinglyLinkedList createInstance(){
return new SinglyLinkedList();
}
}
class DoublyLinkedListTest extends ListTestBase<DoublyLinkedList> {
@Override
protected DoublyLinkedList createInstance(){
return new DoublyLinkedList();
}
}
The nice thing about doing it this way (instead of making one test class which tests all implementations) is that if there are some specific corner cases you'd like to test with one implementation, you can just add more tests to the specific test subclass.