在一个参数化类(junit)中创建多个参数集合。

69
目前,我必须为每个要测试的方法创建一个带参数的测试类,并使用多个不同的输入进行测试。有没有办法将这些内容合并到一个文件中?
现在有一个名为CalculatorTestAdd.java的文件,其中包含一组参数,用于检查Add()函数是否正常工作。是否有可能将这个参数集“连接”到Add()函数,并创建一个额外的参数集,用于Subtract()方法,并将该方法添加到同一个测试类中,从而生成一个名为CalculatorTest.java的文件?

1
很遗憾你使用JUnit... TestNG的@DataProvider正好符合你的需求。 - fge
@fge 是的,但用 JUnit 不算太难 - 我以前做过。你只需要跳过一些麻烦的步骤。 - Bohemian
1
https://github.com/junit-team/junit/wiki/Parameterized-tests - Aliti
2
@DataProvider 可以与 JUnit 一起使用,感谢 https://github.com/TNG/junit-dataprovider/,比 JUnitParams 更好。 - Y.M.
8个回答

68
这个答案与Tarek的回答类似(有关参数化部分),尽管我认为它更具可扩展性。同时解决了您的问题,如果一切正确,您将不会有失败的测试:
@RunWith(Parameterized.class)
public class CalculatorTest {
    enum Type {SUBSTRACT, ADD};
    @Parameters
    public static Collection<Object[]> data(){
        return Arrays.asList(new Object[][] {
          {Type.SUBSTRACT, 3.0, 2.0, 1.0},
          {Type.ADD, 23.0, 5.0, 28.0}
        });
    }

    private Type type;
    private Double a, b, expected;

    public CalculatorTest(Type type, Double a, Double b, Double expected){
        this.type = type;
        this.a=a; this.b=b; this.expected=expected;
    }

    @Test
    public void testAdd(){
        Assume.assumeTrue(type == Type.ADD);
        assertEquals(expected, Calculator.add(a, b));
    }

    @Test
    public void testSubstract(){
        Assume.assumeTrue(type == Type.SUBSTRACT);
        assertEquals(expected, Calculator.substract(a, b));
    }
}

3
一个优雅的解决方案。这就像说“这个测试方法仅适用于这个数据集”。这种做法灵活性很强,可以有例如使用数据集1的2个测试方法和使用数据集2的3个测试方法。 - Henno Vermeulen
2
不错的解决方案 - 但是如果每个测试用例需要不同的构造函数会发生什么? - rj2700
1
@robjob27 我认为你可以为参数设置内部类: { Type.SUBSTRACT, new ParamsSubstract(3,2), 1}, { Type.SQROOT, new ParamsSquareRoot(4), 2}}); Params 类继承自一个公共类 Params,并作为构造函数的一部分: public CalculatorTest(Type type, Params params, Double expected){..} 但这可能会使一切变得过于复杂,在那种情况下,我会考虑是否值得为所有内容创建一个类。 - nessa.gp
我不喜欢这种方法,它有假设,并且基本上我们每次都运行两个测试用例,因此会运行4个测试用例,而我只想要两个测试用例。 - Vinay Prajapati
在同一个测试方法中使用switch-case语句,与枚举进行模式匹配。这样你只需要运行一个单一的测试。 - Core_Dumped

34

在我看来,另一个纯JUnit但优雅的解决方案是将每个参数化测试封装在自己的静态内部类中,并在顶层测试类上使用Enclosed测试运行器。这不仅允许您独立地为每个测试使用不同的参数值,而且还可以使用完全不同参数的方法进行测试。

它会像这样:

@RunWith(Enclosed.class)
public class CalculatorTest {

  @RunWith(Parameterized.class)
  public static class AddTest {

    @Parameters
    public static Collection<Object[]> data() {
      return Arrays.asList(new Object[][] {
          { 23.0, 5.0, 28.0 }
      });
    }

    private Double a, b, expected;

    public AddTest(Double a, Double b, Double expected) {
      this.a = a;
      this.b = b;
      this.expected = expected;
    }

    @Test
    public void testAdd() {
      assertEquals(expected, Calculator.add(a, b));
    }
  }

  @RunWith(Parameterized.class)
  public static class SubstractTest {

    @Parameters
    public static Collection<Object[]> data() {
      return Arrays.asList(new Object[][] {
          { 3.0, 2.0, 1.0 }
      });
    }

    @Parameter(0)
    private Double a;
    @Parameter(1)
    private Double b;
    @Parameter(2)
    private Double expected;

    @Test
    public void testSubstract() {
      assertEquals(expected, Calculator.substract(a, b));
    }
  }

  @RunWith(Parameterized.class)
  public static class MethodWithOtherParametersTest {

    @Parameters
    public static Collection<Object[]> data() {
      return Arrays.asList(new Object[][] {
          { 3.0, 2.0, "OTHER", 1.0 }
      });
    }

    private Double a;
    private BigDecimal b;
    private String other;
    private Double expected;

    public MethodWithOtherParametersTest(Double a, BigDecimal b, String other, Double expected) {
      this.a = a;
      this.b = b;
      this.other = other;
      this.expected = expected;
    }

    @Test
    public void testMethodWithOtherParametersTest() {
      assertEquals(expected, Calculator.methodWithOtherParametersTest(a, b, other));
    }
  }

  public static class OtherNonParameterizedTests {

    // here you can add any other test which is not parameterized

    @Test
    public void otherTest() {
      // test something else
    }
  }
}
请注意在SubstractTest中使用@Parameter注解的用法,我认为这样更易于阅读。但这更多是品味问题。

26
现在JUnit-5为此提供了一种解决方案——通过重新定义编写参数化测试的方式。现在,可以使用@ParameterizedTest在方法级别定义参数化测试,并可以使用@MethodSource给出方法源。 因此,在您的情况下,您可以有2个单独的数据源方法为add()和subtract()测试方法提供输入数据,这两个方法都在同一个类中。您的代码应该像这样:
public class CalculatorTest{
    public static int[][] dataSetForAdd() {
        return new int[][] { { 1 , 2, 3 }, { 2, 4, 6 }, { 121, 4, 125 } };
    }
    
    public static int[][] dataSetForSubtract() {
        return new int[][] { { 1 , 2, -1 }, { 2, 4, -2 }, { 121, 4, 117 } };
    }

    @ParameterizedTest
    @MethodSource(value = "dataSetForAdd")
    void testCalculatorAddMethod(int[] dataSetForAdd) {
        Calculator calculator= new Calculator();
        int m1 = dataSetForAdd[0];
        int m2 = dataSetForAdd[1];
        int expected = dataSetForAdd[2];
        assertEquals(expected, calculator.add(m1, m2));
    }

    @ParameterizedTest
    @MethodSource(value = "dataSetForSubtract")
    void testCalculatorAddMethod(int[] dataSetForSubtract) {
        Calculator calculator= new Calculator();
        int m1 = dataSetForSubtract[0];
        int m2 = dataSetForSubtract[1];
        int expected = dataSetForSubtract[2];
        assertEquals(expected, calculator.subtract(m1, m2));
    }
}

1
我认为,注解@MethodSource已经不再具有字段names,而只有字段value - Datz
更改已经合并。 - Ankit Singodia

18

我确定你不再遇到这个问题,但是我想到了三种方法可以解决此问题,每种方法都有其优缺点。使用Parameterized runner, 您将不得不使用一个变通方法。

- 使用更多参数与Parameterized

如果您必须从外部加载参数,则只需添加一个期望结果的参数。

优点: 编码较少,且它运行所有测试。

缺点: 针对每组不同的测试需要新的参数。

@RunWith(Parameterized.class)
public class CalculatorTest extends TestCase {
    private Calculator calculator;
    private int operator1;
    private int operator2;
    private int expectedSum;
    private int expectedSub;

    public CalculatorTest(int operator1, int operator2, int expectedSum, int expectedSub) {
        this.operator1 = operator1;
        this.operator2 = operator2;
    }

    @Params
    public static Collection<Object[]> setParameters() {
        Collection<Object[]> params = new ArrayList<>();
        // load the external params here
        // this is an example
        params.add(new Object[] {2, 1, 3, 1});
        params.add(new Object[] {5, 2, 7, 3});

        return params;
    }

    @Before
    public void createCalculator() {
        calculator = new Calculator();
    }

    @Test
    public void addShouldAddTwoNumbers() {
        assertEquals(expectedSum, calculator.add(operator1, operator2));
    }

    @Test
    public void subtractShouldSubtractTwoNumbers() {
        assertEquals(expectedSub, calculator.subtract(operator1, operator2));
    }

    @After
    public void endTest() {
        calculator = null;
        operator1 = null;
        operator2 = null;
        expectedSum = null;
        expectedSub = null;
    }
}

- 不使用参数化运行器

如果您通过编程设置参数,那么这是有效的。

优点: 您可以拥有任意数量的测试而无需设置大量的参数。

缺点: 需要编写更多代码,并且它会在第一个失败处停止(这可能不是缺点)。

@RunWith(JUnit4.class)
public class CalculatorTest extends TestCase {
    private Calculator calculator;

    @Before
    public void createCalculator() {
        calculator = new Calculator();
    }

    @Test
    public void addShouldAddTwoNumbers() {
        int[] operator1 = {1, 3, 5};
        int[] operator2 = {2, 7, 9};
        int[] expectedResults = {3, 10, 14};

        for (int i = 0; i < operator1.length; i++) {
            int actualResult = calculator.add(operator1[i], operator2[i]);
            assertEquals(expectedResults[i], actualResult);
        }
    }

    @Test
    public void subtractShouldSubtractTwoNumbers() {
        int[] operator1 = {5, 8, 7};
        int[] operator2 = {1, 2, 10};
        int[] expectedResults = {4, 6, -3};

        for (int i = 0; i < operator1.length; i++) {
            int actualResult = calculator.subtract(operator1[i], operator2[i]);
            assertEquals(expectedResults[i], actualResult);
        }
    }

    @After
    public void endTest() {
        calculator = null;
    }
}

- 使用JUnitParams

我与Pragmatists没有关联,几天前刚发现这个框架。该框架在JUnit基础上运行,以不同的方式处理参数化测试。参数直接传递到测试方法中,因此你可以在同一个类中为不同的方法提供不同的参数。

优点: 可以达到与上述解决方案相同的结果,而无需额外解决方案。

缺点: 也许您的公司不允许向项目添加新依赖项,或者强制您使用某些奇怪的编码规则(例如仅使用Parameterized runners)。让我们面对现实吧,这种情况比我们想象的要多。

这里是JUnitParams的一个很好的示例,您可以在Github页面上获取该项目/检查代码。


如果我有更多的加法操作而不是减法操作,那么这种方法会失败或产生冗余的空数据。 - Vinay Prajapati
是的,使用JUnit 4的参数化运行器(Parameterized runner),您需要为每个测试案例提供相同数量的用例,否则您需要创建两个不同的测试类,并为它们各自提供参数。JUnit 5终于推出了新的@ParameterizedTest注解,允许您为每个测试案例提供不同的值,而不是为整个类提供。 - Tarek

12

您可以使用参数与https://github.com/piotrturski/zohhak

@TestWith({
   "1, 7, 8",
   "2, 9, 11"
})
public void addTest(int number1, int number2, int expectedResult) {
    BigDecimal result = calculator.add(number1, number2);
    assertThat(result).isEqualTo...
}

如果你想从文件中加载参数,可以使用 http://code.google.com/p/fuzztester/http://code.google.com/p/junitparams/

如果你需要真正的灵活性,可以使用JUnit的@Parameterized,但它会使你的代码变得混乱。 另外,你也可以使用JUnit的Theories,但这对于计算器测试来说似乎有些过度。


自Java8开始,可以重复使用相同的注解。你知道这个功能是否存在吗? - OlivierTerrien
我不理解你的问题。你想如何使用重复注释来使用上述任何库来解决OP问题? - piotrek
我想要像以下这样的东西: @TestWith("1, 7, 8") @TestWith("2, 9, 11") public void addTest(int number1, int number2, int expectedResult) { BigDecimal result = calculator.add(number1, number2); assertThat(result).isEqualTo... } - OlivierTerrien
不好意思,Zohhak 不支持它。其他的,我不确定,但我认为不支持。 - piotrek

9
是的,您不需要做任何特殊的事情。对于每组参数值,每个@Test方法仅运行一次,因此只需编写一个测试add()方法和另一个测试subtract()方法。 此外,我可以补充说,提出这种"适用于所有情况"的设计模式要求的人是误导了。这样做没有什么意义-与其如此,还不如雇佣训练有素的猴子。

1
这似乎可以工作,但也意味着我的一半测试失败了。虽然它做到了我想要的,但我是否应该认为这是最好的结果? - Jeroen Vannevel
我将它标记为已解决,因为它确实解决了我的问题,但如果有更优雅的解决方案可以绕过副作用,这些副作用导致我在使用不适合该测试的参数时失败了很多次测试(因此没有给我一个漂亮的绿色进度条),我会很高兴听到它。 - Jeroen Vannevel
1
这就是为什么我添加了最后一段。像这样规定限制只会导致问题。最好的解决方案是拥有多个测试类;一个使用一组参数,另一个使用第二组参数,也许还有一个不使用参数。基本上做你需要做的事情,并告诉你的团队领导为什么这样做。希望他能明白。如果不行,越过他的头并解释为什么他的“编码规则”会引起问题,并且会损害软件的质量并延误项目。我猜你没有使用持续集成...你应该使用它。 - Bohemian
@JeroenVannevel 如果您可以以某种方式区分不适用于特定测试方法的参数,则可以使用assume*一组方法来省略对这些方法的参数运行。然后,它们将被计为“跳过”,就像使用@Ignore一样(但它们可以运行其他参数)。 - Paŭlo Ebermann
@PaŭloEbermann,我有这些区分,如果您能分享如何使用假设,那将是很大的帮助。 - Arjun Prajapati
显示剩余2条评论

6
使用 Junit Jupiter: https://www.petrikainulainen.net/programming/testing/junit-5-tutorial-writing-parameterized-tests/,可以编写参数化测试。
import intf.ICalculator;

public class Calculator implements ICalculator {
    @Override
    public int plus(int a, int b) {return a + b; }

    @Override
    public int minuis(int a, int b) {return a - b;}

    @Override
    public int multy(int a, int b) {return a * b;}

    @Override  // check in junit byZero
    public int divide(int a, int b) {return a / b;}

}

测试类:

import static org.junit.Assert.assertEquals;

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

class CalculatorJupiter5Test {

    Calculator calculator = new Calculator();

    @DisplayName("Should calculate the correct sum")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
    @CsvSource({
            "5, 3, 8",
            "1, 3, 4",
            "6, 6, 12",
            "2, 3, 5"
    })
    void sum(int a, int b, int sum) {
        assertEquals(sum, calculator.plus(a, b) );
    }

    @DisplayName("Should calculate the correct multy")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, multy={2}")
    @CsvSource({
        "5, 3, 15",
        "1, 3, 3",
        "6, 6, 36",
        "2, 3, 6"
    })
    void multy(int a, int b, int multy) {
        assertEquals(multy, calculator.multy(a, b) );
    }

    @DisplayName("Should calculate the correct divide")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, divide={2}")
    @CsvSource({
        "5, 3, 1",
        "14, 3, 4",
        "6, 6, 1",
        "36, 2,  18"
    })
    void divide(int a, int b, int divide) {
        assertEquals(divide, calculator.divide(a, b) );
    }

   @DisplayName("Should calculate the correct divide by zero")
   @ParameterizedTest(name = "{index} => a={0}, b={1}, divide={2}")
   @CsvSource({
      "5, 0, 0",
   })
    void divideByZero(int a, int b, int divide) {
     assertThrows(ArithmeticException.class,
         () -> calculator.divide(a , b),
         () -> "divide by zero");
    }

    @DisplayName("Should calculate the correct minuis")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, minuis={2}")
    @CsvSource({
        "5, 3, 2",
        "1, 3, -2",
        "6, 6, 0",
        "2, 3, -1"
    })
    void minuis(int a, int b, int minuis) {
        assertEquals(minuis, calculator.minuis(a, b) );
    }
}

1
我使用 junitparams,它允许我在每个测试中传递不同的参数集。JunitParams 使用方法返回参数集,在测试中,您提供方法名称作为参数输入的来源,因此更改方法名称将更改数据集。
import com.xx.xx.xx.Transaction;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;


@RunWith(JUnitParamsRunner.class)
public class IpAddressValidatorTest {

    private Validator validator;

    @Before
    public void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();

    }

    public static List<String> goodData() {
        return Arrays.asList(
                "10.10.10.10",
                "127.0.0.1",
                "10.136.182.1",
                "192.168.1.1",
                "192.168.1.1",
                "1.1.1.1",
                "0.0.0.0"
        );
    }

    public static List<String> badData() {
        return Arrays.asList(
                "01.01.01.01",
                "255.255.255.256",
                "127.1",
                "192.168.0.0"
        );
    }

    @Test
    @Parameters(method = "goodData")
    public void ipAddressShouldBeValidated_AndIsValid(String ipAddress) {
        Transaction transaction = new Transaction();
        transaction.setIpAddress(ipAddress);
        Set<ConstraintViolation<Transaction>> violations = validator.validateProperty(transaction, "ipAddress");
        assertTrue(violations.isEmpty());
    }

    @Test
    @Parameters(method = "badData")
    public void ipAddressShouldBeValidated_AndIsNotValid(String ipAddress) {
        Transaction transaction = new Transaction();
        transaction.setIpAddress(ipAddress);
        Set<ConstraintViolation<Transaction>> violations = validator.validateProperty(transaction, "ipAddress");
        assertFalse(violations.isEmpty());
    }


}

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