使用Junit4:如何使用自定义注释过滤出一类测试

问题描述

我有一个自定义批注,可以根据被测设备的特性在运行时过滤掉测试。注释可以应用于测试类和测试方法。

    @Target({ElementType.TYPE,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface PhysicalKeyboardTest {
        boolean keyboardRequired() default false;
    }

要过滤带注释的测试,我有一个自定义测试运行器:

    public class MyTestRunner extends BlockJUnit4ClassRunner {
    public MyTestRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    @Override
    protected List<FrameworkMethod> computeTestMethods() {
        return filterKeyboardRequiredTests(super.computeTestMethods());
    }

    private List<FrameworkMethod> filterKeyboardRequiredTests(List<FrameworkMethod> allTests) {
        // create a List that we can modify
        List<FrameworkMethod> filteredTests = new ArrayList<>(allTests);

        // does the test class require a keyboard?
        if (isKeyboardRequired(getTestClass())) {
            // test class is marked "keyboardRequired",filter out all tests

            // PROBLEM: this code causes test-time 'initializationError'

            filteredTests.clear();
            return filteredTests;
        }

        // for each test: does it require a keyboard?
        for (Iterator<FrameworkMethod> iterator = filteredTests.iterator(); iterator.hasNext(); ) {
            FrameworkMethod test = iterator.next();

            // does the test require a keyboard?
            if (isKeyboardRequired(test)) {
                // test is marked "keyboardRequired",filter it out
                iterator.remove();
            }
        }
        return filteredTests;
    }

    /**
     * Determine if the given test class or test is annotated with {@code keyboardRequired}
     *
     * @param annotatable The test class or test
     * @return True if so annotated
     */
    private boolean isKeyboardRequired(Annotatable annotatable) {
        PhysicalKeyboardTest annotation = annotatable.getAnnotation(PhysicalKeyboardTest.class);
        return annotation != null && annotation.keyboardRequired();
    }

该代码按注释的方式对单个测试方法起作用。

但是如果对测试 class 进行了注释,则在运行测试时,我会得到一个initializationError

java.lang.Exception: No runnable methods
at org.junit.runners.BlockJUnit4ClassRunner.validateInstanceMethods(BlockJUnit4ClassRunner.java:191)
at org.junit.runners.BlockJUnit4ClassRunner.collectInitializationErrors(BlockJUnit4ClassRunner.java:128)
at org.junit.runners.ParentRunner.validate(ParentRunner.java:416)
at org.junit.runners.ParentRunner.<init>(ParentRunner.java:84)
at org.junit.runners.BlockJUnit4ClassRunner.<init>(BlockJUnit4ClassRunner.java:65)
at com.winterberrysoftware.luthierlab.testFramework.MyTestRunner.<init>(MyTestRunner.java:32)
at java.lang.reflect.Constructor.newInstance(Native Method)
at org.junit.internal.builders.AnnotatedBuilder.buildRunner(AnnotatedBuilder.java:104)
at org.junit.internal.builders.AnnotatedBuilder.runnerForClass(AnnotatedBuilder.java:86)
at androidx.test.internal.runner.junit4.AndroidAnnotatedBuilder.runnerForClass(AndroidAnnotatedBuilder.java:63)
at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:26)
at androidx.test.internal.runner.AndroidRunnerBuilder.runnerForClass(AndroidRunnerBuilder.java:153)
at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
at androidx.test.internal.runner.TestLoader.doCreateRunner(TestLoader.java:73)
at androidx.test.internal.runner.TestLoader.getRunnersFor(TestLoader.java:104)
at androidx.test.internal.runner.TestRequestBuilder.build(TestRequestBuilder.java:793)
at androidx.test.runner.AndroidJUnitRunner.buildRequest(AndroidJUnitRunner.java:547)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:390)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1879)

此错误可能是由于computeTestMethods()返回了一个空列表。 (如果返回空值,则失败会更加严重。)

似乎应该在其他地方(可能是在创建测试类列表的地方)对类级别的注释进行过滤,但是我无法找到在哪里做。

感谢您的帮助。

解决方法

我知道了。

除了提供自定义BlockJUnit4ClassRunner之外,我还需要提供自定义AndroidJUnitRunner以及自定义过滤器(与注释相关联)。

添加自定义过滤器

我在自定义注释中添加了一个内部类KeyboardFilter。现在,自定义注释看起来像:

/**
 * This annotation is used to mark tests for devices with a physical
 * keyboard (Chromebook).
 * <br><br>
 * The {@code keyboardRequired} param can be used to flag tests that
 * should not be run if a physical keyboard is not present.
 * <br><br>
 * The annotation can be applied to test classes,and to individual
 * tests.
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhysicalKeyboardTest {
    boolean keyboardRequired() default false;

    /**
     * When {@code keyboardRequired=true} and the test is running on a
     * device that does not have a physical keyboard,filter the test out.
     * <br><br>
     * Will be instantiated via reflection by
     * {@link RunnerArgs.Builder#fromBundle(android.app.Instrumentation,android.os.Bundle)}
     */
    @SuppressWarnings("unused")
    class KeyboardFilter extends ParentFilter {

        /**
         * Determine if the given test can run on the current device,based
         * on the presence of a physical keyboard.
         * <br><br>
         * Tests that are annotated with {@code @PhysicalKeyBoardTest(keyboardRequired=true)}
         * can only be run on devices with a physical keyboard (i.e. Chromebook).
         *
         * @param description the {@link Description} describing the test
         * @return <code>true</code> if test can run
         */
        @Override
        protected boolean evaluateTest(Description description) {
            if (TestHelpers.isChromebook()) {
                return true;
            }
            // we are not running on a Chromebook (i.e. no physical keyboard is attached)

            // check for test-class and test-method annotations
            PhysicalKeyboardTest testAnnotation = description.getAnnotation(PhysicalKeyboardTest.class);
            PhysicalKeyboardTest classAnnotation = description.getTestClass().getAnnotation(PhysicalKeyboardTest.class);

            // if the test-method and test-class are not annotated,the test can run
            return noKeyboardRequired(testAnnotation) && noKeyboardRequired(classAnnotation);
        }

        @Override
        public String describe() {
            return "skip tests annotated with 'PhysicalKeyboardTest(keyboardRequired=true)' " +
                    "if no physical keyboard is present";
        }

        /**
         * Determine if the given annotation says that a keyboard is required.
         *
         * @param annotation The annotation
         * @return True if no keyboard is required
         */
        private boolean noKeyboardRequired(PhysicalKeyboardTest annotation) {
            return annotation == null || !annotation.keyboardRequired();
        }
    }
}

添加自定义检测测试运行器

/**
 * An instrumentation test runner.
 * <br><br>
 * Provides a mechanism for filtering out test classes and test methods,* based on a custom test annotation.
 * <br><br>
 * This class is specified as the {@code testInstrumentationRunner}
 * in the app's {@code build.gradle} file.
 *
 * @see PhysicalKeyboardTest
 * @see androidx.test.internal.runner.RunnerArgs
 */
@SuppressWarnings("unused")
public class MyAndroidJUnitRunner extends AndroidJUnitRunner {
    // androidx.test.internal.runner.RunnerArgs looks for this bundle key
    private static final String FILTER_BUNDLE_KEY = "filter";

    @Override
    public void onCreate(final Bundle bundle) {
        // add the keyboard filter to the test runner's filter list
        bundle.putString(FILTER_BUNDLE_KEY,PhysicalKeyboardTest.KeyboardFilter.class.getName());
        super.onCreate(bundle);
    }
}

指定自定义仪器测试运行器

在应用程序的build.gradle中,我指定了仪器测试运行器:

        testInstrumentationRunner "com.mypath.testFramework.MyAndroidJUnitRunner"

为测试添加注释

有了这个框架,我的包含注释的测试

@PhysicalKeyboardTest(keyboardRequired = true)

将不会在没有键盘的设备上运行。批注可以应用于测试类,也可以应用于单个测试方法。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...