在参数化测试类中排除非参数测试

72

在JUnit中,是否有任何注释可以排除参数化测试类中的非参数测试?


意图是什么?你在测试用例中期望 @Parameters 的使用在哪里? - 卢声远 Shengyuan Lu
9个回答

85

JUnit 5

从JUnit 5.0.0开始,您现在可以使用@ParameterizedTest注释测试方法,而无需使用内部类。除了像下面展示的ValueSource之外,还有许多方法可以向参数化测试提供参数。有关详细信息,请参见官方JUnit用户指南

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

public class ComponentTest {

    @ParameterizedTest
    @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
    public void testCaseUsingParams(String candidate) throws Exception {
    }

    @Test
    public void testCaseWithoutParams() throws Exception {
    }
}

JUnit 4

如果您仍在使用 Junit 4(我已测试过 v4.8.2),您可以将 Enclosed 运行器与内部类和 Parameterized 运行器一起使用:

import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Enclosed.class)
public class ComponentTest {

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

        @Parameters
        ...

        @Test
        public void testCaseUsingParams() throws Exception {
        }
    }

    public static class ComponentSingleTests {

        @Test
        public void testCaseWithoutParams() throws Exception {
        }
    }
}

3
不错的解决方案,你能很好地区分同一模块中的参数化和单个测试。 - Robert
3
不错的解决方案,但是它运行了两次测试!Enclosed.class是Suite.class的扩展,会导致所有Suite运行两次,而不是在这种情况下运行参数的次数。 - Chee Loong Soon
1
我也尝试过这样做。但是我希望nonParametrized测试仅在Parametrized测试之后运行。但我无法使其按照这种方式运行。我也尝试了FixMethodOrder,但没有成功。 - balki
1
太棒了,解决方案非常好。谢谢! - phoenixSid

6
不。最佳实践是将那些非参数化测试移动到不同的类(.java文件)中。

28
一个模块等于一个测试类是另一种最佳实践,但它与这个实践相冲突。它更像是一种变通方法,而不是最佳实践。 - Jilles van Gurp
你对这个说法有什么理由吗?这意味着一个给定的类将在两个不同的测试类中进行测试,这也让我觉得这是一个不好的想法。 - scubbo
1
@scubbo 我不明白为什么在两个不同的类中编写测试是一个坏主意。我甚至在没有参数化测试的情况下为一些类做过这样的事情。例如,Foo_SecurityTest、Foo_BusinessLogicTest。 - Jeanne Boyarsky

4

我能够做类似于Matthew Madson的回答,发现创建一个基类来封装单个测试和参数化测试之间的设置和常见帮助函数非常有用。这可以在不使用Enclosed.class的情况下工作。

 @RunWith(Suite.class)
 @SuiteClasses({ComponentTest.ComponentParamTests.class, ComponentTest.ComponentSingleTests.class})
 public class ComponentTest {

    public static class TestBase {
        @Spy
        ...
        @Before
        ...
    }

    @RunWith(Parameterized.class)
    public static class ComponentParamTests extends TestBase{
        @Parameter
        ...
        @Parameters
        ...
        @Test
        ...
    }
    public static class ComponentSingleTests extends TestBase{
        @Test
        ...
    }
}

好的解决方案。我唯一要补充的是,你可以使用@RunWith(Suite.class)@SuiteClasses({ ComponentTest.ComponentParamTests.class, ComponentTest.ComponentParamTests.class })注释外部类ComponentTest,使其可运行。然后它将运行嵌套类中的所有测试。来源:http://junit.sourceforge.net/javadoc/org/junit/runners/Suite.html - JCoster22
这对于我想要运行此类中的所有测试非常有效,以前我必须选择其中之一。但是,在包/项目级别运行单元测试时,两个类都会运行。谢谢! - Phil Ninan

3

1

看起来TestNG没有遇到这个问题。 我并不是那么绝望,所以我修改了内置的Parameterized类来支持这个特性。只需将适用的测试注释为@NonParameterized即可。 请注意,此类仅适用于其自己的注释,即检查您的导入。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.junit.Test;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.Suite;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;

/**
 * <p>
 * The custom runner <code>Parameterized</code> implements parameterized tests.
 * When running a parameterized test class, instances are created for the
 * cross-product of the test methods and the test data elements.
 * </p>
 * For example, to test a Fibonacci function, write:
 *
 * <pre>
 * &#064;RunWith(Parameterized.class)
 * public class FibonacciTest {
 *     &#064;Parameters
 *     public static List&lt;Object[]&gt; data() {
 *         return Arrays.asList(new Object[][] {
 *                 Fibonacci,
 *                 { {0, 0}, {1, 1}, {2, 1}, {3, 2}, {4, 3}, {5, 5},
 *                         {6, 8}}});
 *     }
 *
 *     private int fInput;
 *
 *     private int fExpected;
 *
 *     public FibonacciTest(int input, int expected) {
 *         fInput = input;
 *         fExpected = expected;
 *     }
 *
 *     &#064;Test
 *     public void test() {
 *         assertEquals(fExpected, Fibonacci.compute(fInput));
 *     }
 * }
 * </pre>
 * <p>
 * Each instance of <code>FibonacciTest</code> will be constructed using the
 * two-argument constructor and the data values in the
 * <code>&#064;Parameters</code> method.
 * </p>
 */
public class Parameterized extends Suite {

    /**
     * Annotation for a method which provides parameters to be injected into the
     * test class constructor by <code>Parameterized</code>
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public static @interface Parameters {
    }

    /**
     * Annotation for a methods which should not be parameterized
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public static @interface NonParameterized {
    }

    private class TestClassRunnerForParameters extends
            BlockJUnit4ClassRunner {
        private final int fParameterSetNumber;

        private final List<Object[]> fParameterList;

        TestClassRunnerForParameters(Class<?> type,
                List<Object[]> parameterList, int i) throws InitializationError {
            super(type);
            fParameterList = parameterList;
            fParameterSetNumber = i;
        }

        @Override
        public Object createTest() throws Exception {
            return getTestClass().getOnlyConstructor().newInstance(
                    computeParams());
        }

        private Object[] computeParams() throws Exception {
            try {
                return fParameterList.get(fParameterSetNumber);
            } catch (ClassCastException e) {
                throw new Exception(String.format(
                        "%s.%s() must return a Collection of arrays.",
                        getTestClass().getName(), getParametersMethod(
                                getTestClass()).getName()));
            }
        }

        @Override
        protected String getName() {
            return String.format("[%s]", fParameterSetNumber);
        }

        @Override
        protected String testName(final FrameworkMethod method) {
            return String.format("%s[%s]", method.getName(),
                    fParameterSetNumber);
        }

        @Override
        protected void validateConstructor(List<Throwable> errors) {
            validateOnlyOneConstructor(errors);
        }

        @Override
        protected Statement classBlock(RunNotifier notifier) {
            return childrenInvoker(notifier);
        }

        @Override
        protected List<FrameworkMethod> computeTestMethods() {
            List<FrameworkMethod> ret = super.computeTestMethods();
            for (Iterator<FrameworkMethod> i = ret.iterator(); i.hasNext();) {
                FrameworkMethod frameworkMethod =
                    (FrameworkMethod) i.next();
                if (isParameterized() ^
                    !frameworkMethod.getMethod().isAnnotationPresent(
                        NonParameterized.class)) {
                    i.remove();
                }
            }
            return ret;
        }

        protected boolean isParameterized() {
            return true;
        }
    }

    private class TestClassRunnerForNonParameterized extends
        TestClassRunnerForParameters {

        TestClassRunnerForNonParameterized(Class<?> type,
            List<Object[]> parameterList, int i)
            throws InitializationError {
            super(type, parameterList, i);
        }

        protected boolean isParameterized() {
            return false;
        }
    }

    private final ArrayList<Runner> runners = new ArrayList<Runner>();

    /**
     * Only called reflectively. Do not use programmatically.
     */
    public Parameterized(Class<?> klass) throws Throwable {
        super(klass, Collections.<Runner> emptyList());
        List<Object[]> parametersList = getParametersList(getTestClass());
        if (parametersList.size() > 0) {
            try {
                runners.add(new TestClassRunnerForNonParameterized(
                    getTestClass()
                        .getJavaClass(), parametersList, 0));
            } catch (Exception e) {
                System.out.println("No non-parameterized tests.");
            }
        }
        try {
            for (int i = 0; i < parametersList.size(); i++) {
                runners.add(new TestClassRunnerForParameters(getTestClass()
                    .getJavaClass(),
                    parametersList, i));
            }
        } catch (Exception e) {
            System.out.println("No parameterized tests.");
        }
    }

    @Override
    protected List<Runner> getChildren() {
        return runners;
    }

    @SuppressWarnings("unchecked")
    private List<Object[]> getParametersList(TestClass klass)
            throws Throwable {
        return (List<Object[]>) getParametersMethod(klass).invokeExplosively(
                null);
    }

    private FrameworkMethod getParametersMethod(TestClass testClass)
            throws Exception {
        List<FrameworkMethod> methods = testClass
                .getAnnotatedMethods(Parameters.class);
        for (FrameworkMethod each : methods) {
            int modifiers = each.getMethod().getModifiers();
            if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))
                return each;
        }

        throw new Exception("No public static parameters method on class "
                + testClass.getName());
    }

}

更新:我正在尝试将这种功能添加到junit中。


1
对于那些希望参数来自Java函数而不是注解的人:
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
    assertEquals(expected, Strings.isBlank(input));
}

private static Stream<Arguments> provideStringsForIsBlank() {
    return Stream.of(
      Arguments.of(null, true),
      Arguments.of("", true),
      Arguments.of("  ", true),
      Arguments.of("not blank", false)
    );
}

来源:https://www.baeldung.com/parameterized-tests-junit-5

(该文章)涉及编程相关内容,介绍了JUnit 5中的参数化测试,并提供了通俗易懂的解释。请注意,保留了HTML标签。

1
我做了类似于Matthew的解决方案。但是,我创建了两个新的Java文件,扩展了当前文件,以便ComponentSingleTests不会运行两次。这样,它们可以从父类共享公共成员变量和辅助方法。我在Matthew的解决方案中遇到的问题是,我的单个测试运行了两次,而不是一次,因为Enclosed.class(它扩展了Suite.class)会让你的测试像在此链接中描述的那样运行两次Prevent junit tests from running twice

ComponentTest.java

public class ComponentTest {
    public int sharedMemberVariables; 
    ... 
}

ComponentParamTests.java

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class ComponentParamTests extends ComponentTest {

    @Parameters
    ...

    @Test
    public void testCaseUsingParams() throws Exception {
    }
}

ComponentSingleTests.java

import org.junit.Test;

public class ComponentSingleTests {

    @Test
    public void testCaseWithoutParams() throws Exception {
        ...
    }
}

这对我很有帮助,因为添加Enclosed.class会导致测试运行两次。 - Akshat

0

我在编写Spring Boot MockMvc测试时遇到了问题。我简单地创建了两个类,分别放在不同的Java文件中(一个用于ParameterizedTest,另一个用于SingleTest),并为它们创建了一个套件。因为内部类对于静态成员、非静态成员和类都会产生错误。


0
假设您使用Parametrized.class来运行测试类-请将所有非参数化测试标记为@Ignored。否则,您可以创建一个静态内部类来分组所有参数化测试,另一个用于非参数化测试。

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