简体   繁体   中英

How to test different implementations for an interface in Junit5 without duplicating the code

May I ask how to write a junit 5 test for an interface with different implementations?

For example, I have a interface Solution , with different implementations like SolutionI , SolutionII , can I write only one test class to test both?

There is a post shows an example, but if there are multiple test method that needs to be called, I have to pass the parameter for every test method.

May I ask if there is an elegant way like what is used in the Junit4

In Junit4, I have a very elegant code sample as follows

@RunWith(Parameterized.class)
public class SolutionTest {
  private Solution solution;

  public SolutionTest(Solution solution) {
    this.solution = solution;
  }

  @Parameterized.Parameters
  public static Collection<Object[]> getParameters() {
    return Arrays.asList(new Object[][]{
        {new SolutionI()},
        {new SolutionII()}
    });
  }
  // normal test methods
  @Test
  public void testMethod1() {

  }
}

Junit 5 claims ExtendWith() is similar, I tried the following code

@ExtendWith(SolutionTest.SolutionProvider.class)
public class SolutionTest {
  private Solution solution;

  public SolutionTest(Solution solution) {
    System.out.println("Call constructor");
    this.solution = solution;
  }

  @Test
  public void testOnlineCase1() {
    assertEquals(19, solution.testMethod(10));
  }

  @Test
  public void testOnlineCase2() {
    assertEquals(118, solution.testMethod(100));
  }

  static class SolutionProvider implements ParameterResolver {
    private final Solution[] solutions = {
        new SolutionI(),
        new SolutionII()
    };
    private static int i = 0;

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
      return parameterContext.getParameter().getType() == Solution.class;
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
      System.out.println(i);
      return solutions[i++];
    }
  }
}

Unfortunately, testMethod1 is using SolutionI and testMethod2 is using SolutionII , which makes sense, I don't know if this helps to inspire a little bit.

Thanks for the help in advance

Jupiter provides Test interfaces exactly for your purpose - to test interface contract .

For example, let's have an interface for string diagnostic contract and two implementations following the contract but exploiting different implementation ideas:

public interface StringDiagnose {
    /**
     * Contract: a string is blank iff it consists of whitespace chars only
     * */
    boolean isTheStringBlank(String string);
}

public class DefaultDiagnose implements StringDiagnose {

    @Override
    public boolean isTheStringBlank(String string) {
        return string.trim().length() == 0;
    }
}

public class StreamBasedDiagnose implements StringDiagnose {

    @Override
    public boolean isTheStringBlank(String string) {
        return string.chars().allMatch(Character::isWhitespace);
    }
}

According to the recommended approach you are to create test interface that verifies the diagnostic contract in default methods and exposes implementation-dependent pieces to hooks:

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;

public interface StringDiagnoseTest<T extends StringDiagnose> {

    T createDiagnose();

    @Test
    default void blankCheckFollowsContract(){
        assertTrue(createDiagnose().isTheStringBlank("\t\n "));
        assertFalse(createDiagnose().isTheStringBlank("\t\n !  \r\n"));
    }
}

and then implement this test interface for each solution specific:

class DefaultDiagnoseTest implements StringDiagnoseTest<DefaultDiagnose> {

    @Override
    public DefaultDiagnose createDiagnose() {
        return new DefaultDiagnose();
    }
}

class StreamBasedDiagnoseTest implements StringDiagnoseTest<StreamBasedDiagnose> {

    @Override
    public StreamBasedDiagnose createDiagnose() {
        return new StreamBasedDiagnose();
    }
}

Use more hooks and not- default interface methods to test same-named solutions' aspects (like performance) and define new tests in the interface implementations for completely distinctive implementation pecularities.

Sorry for not replying to this thread for a while. Comparing to the lotor's answer, I found some other ways I am currently adopting:


  @ParameterizedTest
  @MethodSource("solutionStream")
  void testCase(Solution solution) {
   // add your test
  }

  static Stream<Solution> solutionStream() {
    return Stream.of(
        new SolutionI(),
        new SolutionII()
    );
  }

The constructor needs parameters (Not type-safe)

  @ParameterizedTest
  @MethodSource("solutionStream")
  void testOnlineCase(Class<Solution> solutionClass) throws NoSuchMethodException, IllegalAccessException,
      InvocationTargetException, InstantiationException {
    Solution solution = solutionClass.getConstructor(Integer.TYPE).newInstance(2);
  }

  static Stream<Class> solutionStream() {
    return Stream.of(
        SolutionI.class
    );
  }

For example, I have a interface Solution, with different implementations like SolutionI, SolutionII, can I write only one test class to test both?

The short answer is that you shouldn't do that . As it's best practice that for UT each implementation would have its own test class so that if one implementation changes, then only relevant tests will be impacted. Please find below some additional thoughts:

  • If you have two implementations of the same interface, I guess the logic is different otherwise why bothering having two implementations in the first place? So you should have two set of tests;

  • If you have common logic between the two implementations, then you should put it in an abstract class that would be extended by your implementations.

  • ParameterizedTest shouldn't be abused to deviate from the best pattern;

  • In other to avoid code replication for tests, according to your use case, In JUnit5 You can indeed use extensions as explained here in the documentation.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM