73

JUnit 4.8 包含一个名为“Categories”的不错的新特性,它允许您将某些类型的测试组合在一起。这非常有用,例如,对慢速和快速测试进行单独的测试运行。我知道JUnit 4.8 发行说明中提到的内容,但想知道如何实际运行所有带有特定类别注释的测试。

JUnit 4.8 发行说明显示了一个示例套件定义,其中 SuiteClasses 注释从特定类别中选择要运行的测试,如下所示:

@RunWith(Categories.class)
@IncludeCategory(SlowTests.class)
@SuiteClasses( { A.class, B.class }) // Note that Categories is a kind of Suite
public class SlowTestSuite {
  // Will run A.b and B.c, but not A.a
}

有谁知道我如何运行 SlowTests 类别中的所有测试?看来你必须有 SuiteClasses 注释......

4

6 回答 6

64

我找到了一种可能的方法来实现我想要的,但我不认为这是最好的解决方案,因为它依赖于不属于 JUnit 的 ClassPathSuite 库。

我为这样的慢速测试定义了测试套件:

@RunWith(Categories.class)
@Categories.IncludeCategory(SlowTests.class)
@Suite.SuiteClasses( { AllTests.class })
public class SlowTestSuite {
}

AllTests 类定义如下:

@RunWith(ClasspathSuite.class)
public class AllTests {
}

我不得不在这里使用ClassPathSuite项目中的 ClassPathSuite 类。它将找到所有带有测试的类。

于 2010-02-01T13:04:43.553 回答
7

以下是 TestNG 和 JUnit 在组(或类别,如 JUnit 所称)方面的一些主要区别:

  • JUnit 是类型化的(注解),而 TestNG 是字符串。我做出这个选择是因为我希望能够在运行测试时使用正则表达式,例如“运行属于组“数据库*”的所有测试。此外,每当您需要创建一个新的注释时,都必须创建一个新的注释类别很烦人,尽管它的好处是 IDE 会立即告诉您该类别的使用位置(TestNG 在其报告中向您展示了这一点)。

  • TestNG 将您的静态模型(测试代码)与运行时模型(测试运行)非常清楚地分开。如果您想先运行组“前端”,然后运行“servlet”,则无需重新编译任何内容即可执行此操作。因为 JUnit 在注解中定义了组,并且您需要将这些类别指定为运行程序的参数,所以当您想要运行一组不同的类别时,您通常必须重新编译您的代码,这在我看来违背了目的。

于 2011-06-13T01:28:42.907 回答
6

Kaitsu 解决方案的一个缺点是当运行项目中的所有测试时,Eclipse将运行您的测试两次,而 SlowTests 则运行 3 次。这是因为 Eclipse 将运行所有测试,然后是 AllTests 套件,然后是 SlowTestSuite。

这是一个解决方案,它涉及创建 Kaitsu 解决方案测试运行器的子类以跳过套件,除非设置了某个系统属性。一个可耻的黑客,但到目前为止我想出的只是。

@RunWith(DevFilterClasspathSuite.class)
public class AllTests {}

.

@RunWith(DevFilterCategories.class)
@ExcludeCategory(SlowTest.class)
@SuiteClasses(AllTests.class)
public class FastTestSuite
{
}

.

public class DevFilterCategories extends Suite
{
    private static final Logger logger = Logger
        .getLogger(DevFilterCategories.class.getName());
    public DevFilterCategories(Class<?> suiteClass, RunnerBuilder builder) throws InitializationError {
        super(suiteClass, builder);
        try {
            filter(new CategoryFilter(getIncludedCategory(suiteClass),
                    getExcludedCategory(suiteClass)));
            filter(new DevFilter());
        } catch (NoTestsRemainException e) {
            logger.info("skipped all tests");
        }
        assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription());
    }

    private Class<?> getIncludedCategory(Class<?> klass) {
        IncludeCategory annotation= klass.getAnnotation(IncludeCategory.class);
        return annotation == null ? null : annotation.value();
    }

    private Class<?> getExcludedCategory(Class<?> klass) {
        ExcludeCategory annotation= klass.getAnnotation(ExcludeCategory.class);
        return annotation == null ? null : annotation.value();
    }

    private void assertNoCategorizedDescendentsOfUncategorizeableParents(Description description) throws InitializationError {
        if (!canHaveCategorizedChildren(description))
            assertNoDescendantsHaveCategoryAnnotations(description);
        for (Description each : description.getChildren())
            assertNoCategorizedDescendentsOfUncategorizeableParents(each);
    }

    private void assertNoDescendantsHaveCategoryAnnotations(Description description) throws InitializationError {           
        for (Description each : description.getChildren()) {
            if (each.getAnnotation(Category.class) != null)
                throw new InitializationError("Category annotations on Parameterized classes are not supported on individual methods.");
            assertNoDescendantsHaveCategoryAnnotations(each);
        }
    }

    // If children have names like [0], our current magical category code can't determine their
    // parentage.
    private static boolean canHaveCategorizedChildren(Description description) {
        for (Description each : description.getChildren())
            if (each.getTestClass() == null)
                return false;
        return true;
    }
}

.

public class DevFilterClasspathSuite extends ClasspathSuite
{
    private static final Logger logger = Logger
        .getLogger(DevFilterClasspathSuite.class.getName());
    public DevFilterClasspathSuite(Class<?> suiteClass, RunnerBuilder builder) 
        throws InitializationError {
        super(suiteClass, builder);
        try
        {
            filter(new DevFilter());
        } catch (NoTestsRemainException e)
        {
            logger.info("skipped all tests");
        }
    }
}

.

public class DevFilter extends Filter
{
    private static final String RUN_DEV_UNIT_TESTS = "run.dev.unit.tests";

    @Override
    public boolean shouldRun(Description description)
    {
        return Boolean.getBoolean(RUN_DEV_UNIT_TESTS);
    }

    @Override
    public String describe()
    {
        return "filter if "+RUN_DEV_UNIT_TESTS+" system property not present";
    }
}

因此,在您的 FastTestSuite 启动器中,只需将 -Drun.dev.unit.tests=true 添加到 VM 参数中。(请注意,此解决方案引用的是快速测试套件而不是慢速测试套件。)

于 2013-01-11T23:05:39.010 回答
2

要运行分类测试而不在@Suite.SuiteClasses注释中明确指定所有测试,您可以提供自己的 Suite 实现。例如 aorg.junit.runners.ParentRunner可以扩展。@Suite.SuiteClasses新实现应该在类路径中搜索分类测试,而不是使用由 提供的类数组。

将此项目视为此类方法的示例。用法:

@Categories(categoryClasses = {IntegrationTest.class, SlowTest.class})
@BasePackage(name = "some.package")
@RunWith(CategorizedSuite.class)
public class CategorizedSuiteWithSpecifiedPackage {

}
于 2012-11-09T20:27:31.243 回答
1

我不确定,你的问题到底是什么。

只需将所有测试添加到一个套件(或多个套件)中。然后使用 Categories Runner 和 Include/ExcludeCategory 注释来指定要运行的类别。

一个好主意可能是拥有一个包含所有测试的套件,以及引用第一个的几个单独的套件,指定您需要的不同类别集。

于 2010-02-01T12:43:41.997 回答
0

不是您问题的直接答案,但也许可以改进一般方法......

为什么你的测试很慢?也许设置持续很长时间(数据库、I/O 等),也许测试测试太多了?如果是这种情况,我会将真正的单元测试与“长期运行”的单元测试分开,后者通常确实是集成测试。

在我的设置中,我有 staging env,单元测试经常运行,集成测试不断但很少(例如在版本控制中的每次提交之后)。我从来没有为单元测试使用过分组,因为它们应该松散耦合在一起。我只处理集成测试设置中测试用例的分组和关系(但使用 TestNG)。

但很高兴知道 JUnit 4.8 引入了一些分组功能。

于 2010-02-01T20:01:11.213 回答