Writing a single unit test for multiple implementations of an interface

后端 未结 7 1661
感动是毒
感动是毒 2020-12-23 11:26

I have an interface List whose implementations include Singly Linked List, Doubly, Circular etc. The unit tests I wrote for Singly should do good for most of Do

相关标签:
7条回答
  • 2020-12-23 11:51

    You could actually create a helper method in your test class that sets up your test List to be an instance of one of your implementations dependent on an argument. In combination with this you should be able to get the behaviour you want.

    0 讨论(0)
  • 2020-12-23 11:58

    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);
    }
    
    0 讨论(0)
  • 2020-12-23 12:01

    I had exactly the same problem and here is my approach with help of JUnit parameterized tests (based on @dasblinkenlight's answer).

    1. Create a base class for the all test classes:
    @RunWith(value = Parameterized.class)
    public class ListTestUtil {
        private Class<?> listClass = null;
    
        public ListTestUtil(Class<?> listClass) {
            this.listClass = listClass;
        }
    
        /**
         * @return a {@link Collection} with the types of the {@link List} implementations.
         */
        @Parameters
        public static Collection<Class<?>> getTypesData() {
            return List.of(MySinglyLinkedList.class, MyArrayList.class);
        }
    
        public <T> List<Integer> initList(Object... elements) {
            return initList(Integer.class, elements);
        }
    
        @SuppressWarnings("unchecked")
        public <T> List<T> initList(Class<T> type, Object... elements) {
            List<T> myList = null;
            try {
                myList = (List<T>) listClass.getDeclaredConstructor().newInstance();
                for (Object el : elements)
                    myList.add(type.cast(el));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return myList;
        }
    }
    
    1. Classes the contain test cases extend ListTestUtil and you can just use initList(...) wherever you want:
    public class AddTest extends ListTestUtil {
        public AddTest(Class<?> cl) {
            super(cl);
        }
    
        @Test
        public void test1() {
            List<Integer> myList = initList(1, 2, 3);
            // List<Integer> myList = initList(Strng.class, "a", "b", "c");
            ...
            System.out.println(myList.getClass());
        }
    }
    

    The output proves that the test is called twice - once for each implementation of the list:

    class java.data_structures.list.MySinglyLinkedList
    class java.data_structures.list.MyArrayList
    
    0 讨论(0)
  • 2020-12-23 12:02

    With JUnit 4.0+ you can use parameterized tests:

    • Add @RunWith(value = Parameterized.class) annotation to your test fixture
    • Create a public static method returning Collection, annotate it with @Parameters, and put SinglyLinkedList.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 the Class in an instance variable listClass
    • In the setUp method or @Before, use List 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.

    0 讨论(0)
  • 2020-12-23 12:04

    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();
        }
    
    0 讨论(0)
  • 2020-12-23 12:06

    Expanding on the first answer, the Parameter aspects of JUnit4 work very well. Here is the actual code I used in a project testing filters. The class is created using a factory function (getPluginIO) and the function getPluginsNamed gets all PluginInfo classes with the name using SezPoz and annotations to allow for new classes to be automatically detected.

    @RunWith(value=Parameterized.class)
    public class FilterTests {
     @Parameters
     public static Collection<PluginInfo[]> getPlugins() {
        List<PluginInfo> possibleClasses=PluginManager.getPluginsNamed("Filter");
        return wrapCollection(possibleClasses);
     }
     final protected PluginInfo pluginId;
     final IOPlugin CFilter;
     public FilterTests(final PluginInfo pluginToUse) {
        System.out.println("Using Plugin:"+pluginToUse);
        pluginId=pluginToUse; // save plugin settings
        CFilter=PluginManager.getPluginIO(pluginId); // create an instance using the factory
     }
     //.... the tests to run
    

    Note it is important (I personally have no idea why it works this way) to have the collection as a collection of arrays of the actual parameter fed to the constructor, in this case a class called PluginInfo. The wrapCollection static function performs this task.

    /**
     * Wrap a collection into a collection of arrays which is useful for parameterization in junit testing
     * @param inCollection input collection
     * @return wrapped collection
     */
    public static <T> Collection<T[]> wrapCollection(Collection<T> inCollection) {
        final List<T[]> out=new ArrayList<T[]>();
        for(T curObj : inCollection) {
            T[] arr = (T[])new Object[1];
            arr[0]=curObj;
            out.add(arr);
        }
        return out;
    }
    
    0 讨论(0)
提交回复
热议问题