当前位置: 首页 > 面试题库 >

如何在JUnit 5中实现JUnit 4参数化测试?

轩辕天佑
2023-03-14
问题内容

在JUnit
4中,使用@Parameterized批注很容易在多个类中测试不变式。关键是要针对单个参数列表运行一组测试。

如何在不使用JUnit-vintage的情况下在JUnit 5中复制它?

@ParameterizedTest不适用于测试课程。@TestTemplate听起来似乎很合适,但是注释的目标也是一种方法。

此类JUnit 4测试的示例是:

@RunWith( Parameterized.class )
public class FooInvariantsTest{

   @Parameterized.Parameters
   public static Collection<Object[]> data(){
       return new Arrays.asList(
               new Object[]{ new CsvFoo() ),
               new Object[]{ new SqlFoo() ),
               new Object[]{ new XmlFoo() ),
           );
   }

   private Foo fooUnderTest;


   public FooInvariantsTest( Foo fooToTest ){
        fooUnderTest = fooToTest;
   }

   @Test
   public void testInvariant1(){
       ...
   }

   @Test
   public void testInvariant2(){
       ...
   } 
}

问题答案:

JUnit 5中的参数化测试功能所提供的
功能与JUnit
4所提供的功能完全不同。引入了具有更大灵活性的新功能…但是它也失去了JUnit4功能,其中参数化测试类使用参数化的固定装置/断言在该类的所有测试方法的类级别上。因此需要通过指定“输入”来
定义@ParameterizedTest每种测试方法。
除了上述不足之外,我还将介绍两个版本之间的主要区别以及如何在JUnit 5中使用参数化测试。

TL; DR

要编写一个参数化测试以按问题指定一个值来进行测试,以解决您的问题,
org.junit.jupiter.params.provider.MethodSource应该可以完成这项工作。

@MethodSource允许您引用测试类的一种或多种方法。每个方法必须返回一个StreamIterableIterator,或的参数阵列。另外,每个方法都不能接受任何参数。默认情况下,除非使用注释测试类,否则这些方法必须是静态的@TestInstance(Lifecycle.PER_CLASS)

如果只需要一个参数,则可以直接返回该参数类型的实例,如以下示例所示。

作为JUnit 4,它@MethodSource依赖于工厂方法,也可以用于指定多个参数的测试方法。

在JUnit 5中,这是编写最接近JUnit 4的参数化测试的方式。

JUnit 4:

@Parameters
public static Collection<Object[]> data() {

JUnit 5:

private static Stream<Arguments> data() {

主要改进:

  • Collection<Object[]>更是成为Stream<Arguments>提供更多的灵活性。

  • 将工厂方法绑定到测试方法的方式略有不同。
    现在它更短,更不容易出错:不再需要创建构造函数并声明字段来设置每个参数的值。直接在测试方法的参数上完成源的绑定。

  • 对于JUnit 4,在同一类中,必须使用声明一个并且只有一个工厂方法@Parameters
    在JUnit 5中,此限制已解除:确实可以将多种方法用作工厂方法。
    因此,在类内部,我们可以声明一些注释了@MethodSource("..")不同工厂方法的测试方法。

例如,下面是一个示例测试类,它声明了一些附加计算:

import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;    
import org.junit.jupiter.api.Assertions;

public class ParameterizedMethodSourceWithArgumentsTest {

  @ParameterizedTest
  @MethodSource("addFixture")
  void add(int a, int b, int result) {
     Assertions.assertEquals(result, a + b);
  }

  private static Stream<Arguments> addFixture() {
    return Stream.of(
      Arguments.of(1, 2, 3),
      Arguments.of(4, -4, 0),
      Arguments.of(-3, -3, -6));
  }
}

要将现有的参数化测试从JUnit 4升级到JUnit 5,@MethodSource是可以考虑的选择。

总结

@MethodSource既有优点也有缺点。
在JUnit 5中引入了指定参数化测试源的新方法。
在这里,我希望能提供一些有关它们的更多信息(到目前为止还很详尽),以期就如何以一般方式进行处理提供一个广泛的思路。

介绍

JUnit 5 在这些方面引入了参数化测试功能:

通过参数化测试,可以使用不同的参数多次运行测试。它们的声明与常规@Test方法一样,但是使用@ParameterizedTest批注。另外,您必须声明至少一个源,它将为每次调用提供参数。

依赖性要求

参数化测试功能不包含在junit-jupiter-engine核心依赖项中。
您应该添加一个特定依赖使用它:junit-jupiter-params

如果使用Maven,则这是要声明的依赖项:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.0.0</version>
    <scope>test</scope>
</dependency>

可用于创建数据的来源

与JUnit 4相反,JUnit 5提供了多种风格和工件来编写参数化测试
。支持的方式通常取决于要使用的数据源。

这是框架提出的并在文档中描述的源类型:

  • @ValueSource
  • @EnumSource
  • @MethodSource
  • @CsvSource
  • @CsvFileSource
  • @ArgumentsSource

这是我实际上与JUnit 5一起使用的3个主要资源,我将介绍:

  • @MethodSource
  • @ValueSource
  • @CsvSource

在编写参数化测试时,我认为它们是基本的。他们应该允许在JUnit 5中编写您描述的JUnit 4测试的类型。
@EnumSource@ArgumentsSource以及@CsvFileSource 可能的当然是有用的,但他们更专业。

的介绍@MethodSource@ValueSource@CsvSource

1) @MethodSource

这种类型的源需要定义工厂方法。
但这也提供了很大的灵活性。

在JUnit 5中,这是编写最接近JUnit 4的参数化测试的方式。

如果您在测试方法中只有一个 方法参数 ,并且想要使用 任何类型 作为源, @MethodSource则是一个很好的选择。
要实现此目的,请定义一个方法,该方法返回每种情况下的值的流,并使用此数据源方法的名称@MethodSource("methodName")在哪里注释测试methodName方法。

例如,您可以编写:

import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

public class ParameterizedMethodSourceTest {

    @ParameterizedTest
    @MethodSource("getValue_is_never_null_fixture")
    void getValue_is_never_null(Foo foo) {
       Assertions.assertNotNull(foo.getValue());
    }

    private static Stream<Foo> getValue_is_never_null_fixture() {
       return Stream.of(new CsvFoo(), new SqlFoo(), new XmlFoo());
    }

}

如果测试方法中有 多个方法参数 ,并且想要使用 任何类型 作为源, @MethodSource这也是一个很好的选择。
要实现它,请定义一个方法,该方法org.junit.jupiter.params.provider.Arguments针对每种情况返回一个Stream
进行测试。

例如,您可以编写:

import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;    
import org.junit.jupiter.api.Assertions;

public class ParameterizedMethodSourceWithArgumentsTest {

    @ParameterizedTest
    @MethodSource("getFormatFixture")
    void getFormat(Foo foo, String extension) {
        Assertions.assertEquals(extension, foo.getExtension());
    }

    private static Stream<Arguments> getFormatFixture() {
    return Stream.of(
        Arguments.of(new SqlFoo(), ".sql"),
        Arguments.of(new CsvFoo(), ".csv"),
        Arguments.of(new XmlFoo(), ".xml"));
    }
}

2)@ValueSource

如果你有一个 单一的方法参数 在测试方法中,而可能从代表参数的源极 中的一个,这些内置类型(字符串,整数,长,双)
@ValueSource西服。

@ValueSource 确实定义了以下属性:

String[] strings() default {};
int[] ints() default {};
long[] longs() default {};
double[] doubles() default {};

例如,您可以通过以下方式使用它:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class ParameterizedValueSourceTest {

    @ParameterizedTest
    @ValueSource(ints = { 1, 2, 3 })
    void sillyTestWithValueSource(int argument) {
        Assertions.assertNotNull(argument);
    }

}

注意1)您不得指定多个注释属性。
注意2)方法的源和参数之间的映射可以在两种不同的类型之间完成。用作数据源
的类型String尤其可以通过其解析将其转换为多种其他类型。

3) @CsvSource

如果测试方法中有 多个方法参数 ,则@CsvSource可能适合。
要使用它,请用注释测试,@CsvSource并在String每种情况的数组中指定。
每种情况的值均以逗号分隔。

像一样@ValueSource,可以在两种不同的类型之间完成方法的源和参数之间的映射。
以下示例说明了这一点:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

public class ParameterizedCsvSourceTest {

    @ParameterizedTest
    @CsvSource({ "12,3,4", "12,2,6" })
    public void divideTest(int n, int d, int q) {
       Assertions.assertEquals(q, n / d);
    }

}

@CsvSource VS @MethodSource

这些源类型有一个非常经典的要求:在测试方法中从源映射到 多个方法参数
但是他们的方法是不同的。

@CsvSource具有一些优点:它更清晰,更短。
确实,参数是在测试方法的上方定义的,不需要创建夹具方法,该方法还可能会生成“未使用”的警告。
但是它在映射类型方面也有一个重要的限制。
您必须提供的数组String。该框架提供了转换功能,但受到限制。

总而言之,虽然String提供的源代码和测试方法的参数具有相同的类型(String-> String)或依赖于内置转换(例如String->
int),@CsvSource但它是使用的方式。

并非如此,您必须在为@CsvSource创建ArgumentConverter非框架执行的转换创建自定义转换器(子类)
使用@MethodSourcereturn的工厂方法来保持灵活性之间进行选择Stream<Arguments>
它具有上述缺点,但从源到参数的任何类型的开箱即用映射也具有很大的好处。

参数转换

关于源(@CsvSource@ValueSource例如)和测试方法的参数之间的映射,如所看到的,如果类型不同,则框架允许进行一些转换。

以下是两种转换类型的介绍:

3.13.3。 参数转换

隐式转换

为了支持诸如的使用案例@CsvSource,JUnit Jupiter提供了许多内置的隐式类型转换器。转换过程取决于每个方法参数的声明类型。

.....

String 实例当前隐式转换为以下目标类型。

Target Type          |  Example
boolean/Boolean      |  "true" → true
byte/Byte            |  "1" → (byte) 1
char/Character       |  "o" → 'o'
short/Short          |  "1" → (short) 1
int/Integer          |  "1" → 1
.....

例如,在前面的示例中,在String从源到int定义为参数之间进行了隐式转换:

@CsvSource({ "12,3,4", "12,2,6" })
public void divideTest(int n, int d, int q) {
   Assertions.assertEquals(q, n / d);
}

在这里,隐式转换是从String源到LocalDate参数的:

@ParameterizedTest
@ValueSource(strings = { "2018-01-01", "2018-02-01", "2018-03-01" })
void testWithValueSource(LocalDate date) {
    Assertions.assertTrue(date.getYear() == 2018);
}

如果对于两种类型,框架没有提供任何转换(自定义类型就是这种情况),则应使用ArgumentConverter

显式转换

代替使用隐式参数转换,可以像下面的示例一样,ArgumentConverter通过@ConvertWith注释显式指定要用于特定参数的 。

JUnit为需要创建特定客户端的客户提供参考实现ArgumentConverter

显式参数转换器应由测试作者实现。因此,junit-jupiter-params仅提供单个显式参数转换器,该转换器也可以用作参考实现:
JavaTimeArgumentConverter。通过组合注释使用 JavaTimeConversionPattern

使用此转换器的测试方法:

@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
    assertEquals(2017, argument.getYear());
}

JavaTimeArgumentConverter 转换器类别:

package org.junit.jupiter.params.converter;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZonedDateTime;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.ChronoLocalDateTime;
import java.time.chrono.ChronoZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalQuery;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.junit.jupiter.params.support.AnnotationConsumer;

/**
 * @since 5.0
 */
class JavaTimeArgumentConverter extends SimpleArgumentConverter
        implements AnnotationConsumer<JavaTimeConversionPattern> {

    private static final Map<Class<?>, TemporalQuery<?>> TEMPORAL_QUERIES;
    static {
        Map<Class<?>, TemporalQuery<?>> queries = new LinkedHashMap<>();
        queries.put(ChronoLocalDate.class, ChronoLocalDate::from);
        queries.put(ChronoLocalDateTime.class, ChronoLocalDateTime::from);
        queries.put(ChronoZonedDateTime.class, ChronoZonedDateTime::from);
        queries.put(LocalDate.class, LocalDate::from);
        queries.put(LocalDateTime.class, LocalDateTime::from);
        queries.put(LocalTime.class, LocalTime::from);
        queries.put(OffsetDateTime.class, OffsetDateTime::from);
        queries.put(OffsetTime.class, OffsetTime::from);
        queries.put(Year.class, Year::from);
        queries.put(YearMonth.class, YearMonth::from);
        queries.put(ZonedDateTime.class, ZonedDateTime::from);
        TEMPORAL_QUERIES = Collections.unmodifiableMap(queries);
    }

    private String pattern;

    @Override
    public void accept(JavaTimeConversionPattern annotation) {
        pattern = annotation.value();
    }

    @Override
    public Object convert(Object input, Class<?> targetClass) throws ArgumentConversionException {
        if (!TEMPORAL_QUERIES.containsKey(targetClass)) {
            throw new ArgumentConversionException("Cannot convert to " + targetClass.getName() + ": " + input);
        }
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
        TemporalQuery<?> temporalQuery = TEMPORAL_QUERIES.get(targetClass);
        return formatter.parse(input.toString(), temporalQuery);
    }

}


 类似资料:
  • 在JUnit 4中,通过使用注释很容易跨一堆类测试不变量。关键是一组测试正在针对单个参数列表运行。 如何在JUnit 5中复制这一点,而不使用JUnit-vintage? 不适用于测试类。听起来可能是合适的,但该注释的目标也是一个方法。 此类 JUnit 4 测试的一个示例是:

  • 主要内容:1 参数化测试的介绍,2 使用@Parameter进行字段注入而不是构造函数,3 使用单个参数进行测试,4 识别单个测试用例1 参数化测试的介绍 自定义流道参数化实现参数化测试。运行参数化测试类时,将为测试方法和测试数据元素的叉积创建实例。 例如,要测试斐波那契函数,请编写: FibonacciTest的每个实例都将使用二元参数构造函数和方法中的数据值构造 @Parameters 。 2 使用@Parameter进行字段注入而不是构造函数 也可以将数据值直接注入字段中,而无需使用@Pa

  • 我有一个接口,例如: 以及我试图测试的该接口的几个实现(例如、、)。 我的许多测试方法实际上是为了确保接口被正确实现,因此在每个实现中都是重复的。在JUnit3中,一个常见的解决方案是创建一个基类(扩展),然后由每个实现类对其进行子类化。但是对于JUnit4来说,这是正确的方法吗? 在(我相信)优先顺序的升序中可能的选择: > 剪切'n'粘贴重复的测试方法。一点也不枯燥,但我想测试中的担忧比生产代

  • 我有一个工作环境,包括我不管理的bom和JUnit5测试,除非我从如果我从它们不会被maven surefire插件拾取。我阅读了许多部分,并在一个较小的项目中进行了测试,它的工作和拾取,我不知道该在这篇文章中包含什么,因此您有足够的信息来查看问题所在。此外,如果有人能解释我阅读的导入的幕后内容,JUnit 4和5都需要使用surefire即 与版本 我很感激。 注意到 < li >我的测试都在s

  • 我正在尝试从CSV文件运行参数化测试。 如果我只使用这样的CSVSource,它就会起作用: 但如果我从一个文件中尝试同样的方法,它将不起作用: 我也尝试过使用硬路径访问我的文件,但对于Eclipse中的文件测试,我总是能得到消息 找不到使用测试runnter“JUnit 5”的测试。 JUnit期望文件在哪里? 以下是我的依赖关系: 有人知道我可能遗漏了什么或者错误在哪里吗? 提前谢谢你 保罗

  • 我试图定义一个,如示例MockitoExtension所提供的,但无法成功地使用参数化的类实例。 期待着关于如何用在中实现接口的类的参数化实例测试接口的建议。