如何在不复制代码的情况下测试 Junit5 中接口的不同实现

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

请问如何为不同实现的接口编写junit 5测试?

例如,我有一个接口 Solution,具有不同的实现,例如 SolutionISolutionII,我可以只编写一个测试 class 来测试两者吗?

有一个例子,但是如果有多个测试方法需要调用,我必须为每个测试方法传递参数。

请问有没有像Junit4

中使用的优雅方式

在Junit4中,我有一个非常优雅的代码示例如下

@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()类似,我试了下面的代码

@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++];
    }
  }
}

很遗憾,testMethod1用的是SolutionItestMethod2用的是SolutionII,说得有道理,不知道对大家有没有一点启发.

提前感谢您的帮助

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

简短的回答是你不应该那样做。作为最佳实践,对于 UT,每个实现都有自己的测试 class,这样如果一个实现发生变化,那么只有相关测试会受到影响。 请在下面找到一些额外的想法:

  • 如果同一个接口有两个实现,我猜 逻辑是不同的,否则为什么要有两个实现 首先?所以你应该有两组测试;

  • 如果您在两个实现之间有共同的逻辑,那么您应该将其放在一个抽象的 class 中,您的实现将对其进行扩展。

  • 不应滥用参数化测试来偏离最佳模式;

  • 在其他情况下,为了避免测试代码复制,根据您的用例,在 JUnit5 中,您确实可以在文档中使用 extensions as explained here

Jupiter 提供 Test interfaces 正是为了您的目的 - 测试 接口契约

例如,让我们有一个字符串诊断合约的接口和两个遵循合约但利用不同实现思想的实现:

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);
    }
}

根据推荐的方法,您将创建 测试接口,用于验证 default 方法中的诊断 contract 并公开挂钩的实现相关部分:

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"));
    }
}

然后为每个特定的解决方案实施此测试接口

class DefaultDiagnoseTest implements StringDiagnoseTest<DefaultDiagnose> {

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

class StreamBasedDiagnoseTest implements StringDiagnoseTest<StreamBasedDiagnose> {

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

使用更多挂钩和非default接口方法来测试同名解决方案的方面(如性能),并在接口实现中定义新测试以获得完全不同的实现特性。

很抱歉有一段时间没有回复这个话题。对比楼主的回答,我发现了一些我目前采用的其他方式:


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

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

构造函数需要参数(类型不安全)

  @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
    );
  }