如何在Junit5中测试不同接口实现而不复制代码

6

请问如何编写适用于具有不同实现的接口的junit5测试?

例如,我有一个名为Solution的接口,有不同的实现,如SolutionISolutionII,我能否编写一个测试类来测试这两个实现吗?

有一篇帖子提供了一个示例,但如果有多个需要被调用的测试方法,我必须为每个测试方法传递参数。

请问是否有一种像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声称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 使用了 SolutionI,而 testMethod2 使用了 SolutionII,这很合理,我不知道这是否有助于启发一点点。

提前感谢您的帮助。


所以你想知道如何在JUnit 5中定义参数化测试。好吧,JUnit 5用户指南有一个完整的章节专门介绍参数化测试:https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests - JB Nizet
3个回答

11

Jupiter提供测试接口,正好可以用于测试接口契约

例如,我们有一个字符串诊断契约的接口,并且有两个实现遵循该契约但利用不同的实现思路:

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

根据推荐的方法,您需要创建一个测试接口,以验证默认方法中的诊断协定,并将实现相关部分暴露给钩子。请注意保留HTML标签。
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接口方法来测试同名解决方案的方面(如性能),并在接口实现中定义新的测试,以完全区分实现特点。


也许是我的描述不够明确,我并不是在寻找一种测试接口中默认方法的方式。 - Checkwhei Sin
1
@CheckwheiSin,这很可能正是你要找的。这不是测试“默认”方法,而是应该执行的测试方法。它可以根据您的用例进行转换:StringDiagnose-> SolutionDefaultDiagnose-> SolutionIStreamBasedDiagnose-> SolutionII。还要看一下这个答案,可能更容易理解。 - Marcono1234
这比使用参数化测试来测试相同接口的不同实现更好。这样更清晰,对于每个实现,所有测试都是相同的。 - Stuart

1

很抱歉一段时间没有回复这个帖子。与lotor的答案相比,我发现了一些其他方法,目前正在采用:


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

0

我尝试了lotor,但对我来说从一开始就不起作用,我找到了两种方法使其工作。

jupiter: 5.9.2(2023年1月10日)https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api

1-在测试接口中声明实现类。 代码示例:

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

2 - 如果您使用Intelligent IDEA,请检查构建/运行配置,它应该如下所示:

对于属性“要搜索测试的资源类型”,请选择模式和值:

my_test_package.StringDiagnoseTest$DefaultDiagnoseTest||
my_test_package.StringDiagnoseTest$StreamBasedDiagnoseTest

如果你遇到问题,默认情况下选择的类和值为StringDiagnoseTest

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接