junit4 icon indicating copy to clipboard operation
junit4 copied to clipboard

Automatic discovery of @Category annotations

Open mattinger opened this issue 14 years ago • 17 comments

It would be great if I did not have to specifically enumerate all the test classes in a test suite in order to run a specific category. It seems counter intuitive. The only use that @Category really has at that point is at the method level, not at the class level.

I came up with a spring based solution (doesn't really require much of spring, but it uses some spring utility classes, in particular ClassPathScanningCandidateComponentProvider). Perhaps something similar could be implemented in JUnit. The downside to this of course is that the @Category annotation (or some other annotation) has to be at the class level in order for the class to be found. That being said, we might be able to leave out the filter on the scanning provider, though you run the risk of pulling in too much if you do that.

@RunWith(DiscoverCategories.class) @IncludeCategory(Foo.class) @ScanPackage public class MyTestClass { ... }

And in DiscoverCategories class it's relatively easy to find all the classes in the classpath with the @Category annotation:

/***
 * Scan for classes for a given test suite.  This will use the {@link ScanPackage}
 * annotation to look for classes with the {@link Category} annotation.
 * 
 * @param suiteClass
 * @return
 * @throws InitializationError
 */
private static Class<?>[] scanClassesWithCategoryAnnotation(Class<?> suiteClass) 
    throws InitializationError {

    String packages[] = null;
    if (suiteClass.isAnnotationPresent(ScanPackage.class)) {
        packages = suiteClass.getAnnotation(ScanPackage.class).value();
    }

    return scanClassesWithCategoryAnnotation(packages);
}

    /***
 * Scan for classes for a given set of packages.  This will look for classes
 * with the {@link Category} annotation.
 * 
 * @param suiteClass
 * @return
 * @throws InitializationError
 */
private static Class<?>[] scanClassesWithCategoryAnnotation(String packages[]) 
    throws InitializationError {

    ClassPathScanningCandidateComponentProvider provider =
        new ClassPathScanningCandidateComponentProvider(false);
    provider.addIncludeFilter(new AnnotationTypeFilter(Category.class));

    Set<BeanDefinition> allDefs = new HashSet<BeanDefinition>();


    if (packages != null) {
        for (String pack : packages) {
            Set<BeanDefinition> defs = provider.findCandidateComponents(pack);
            allDefs.addAll(defs);
        }
    }

    Set<Class<?>> klasses = new HashSet<Class<?>>();        
    for (BeanDefinition def : allDefs) {
        String beanClassName = def.getBeanClassName();
        try {
            Class<?> klass = Thread.currentThread().getContextClassLoader()
                .loadClass(beanClassName);
            klasses.add(klass);
        }
        catch (ClassNotFoundException e) {
            throw new InitializationError(e);
        }
    }

    return klasses.toArray(new Class<?>[klasses.size()]);
}

/***
 * Public constructor for a test suite class.
 * 
 * @param klass
 * @param builder
 * @throws InitializationError
 */
public DiscoverCategories(Class<?> klass, RunnerBuilder builder)
        throws InitializationError {
    super(builder, klass, scanClassesWithCategoryAnnotation(klass));
    try {
        filter(getCategoryFilter(klass));
    }
    catch (NoTestsRemainException e) {
        throw new InitializationError(e);
    }
}

mattinger avatar Jun 20 '11 14:06 mattinger

FYI: After testing, the annotation filter cannot be left out of spring's scanning provider. If it's left out the scanner doesn't find any classes.

mattinger avatar Jun 20 '11 14:06 mattinger

Have you seen ClasspathSuite? http://johanneslink.net/projects/cpsuite.jsp

kcooney avatar Jun 20 '11 15:06 kcooney

If you still wanted to use JUnit4-style categories, you could do:

import org.junit.extensions.cpsuite.ClasspathSuite.*;

@ClassnameFilters({ "!.*Tests" })
public class AllTests {}

And then do:

@RunWith(Categories.class)
@IncludeCategory(Fast.class)
@SuiteClasses({ AllTests.class })
public class FastTests {}

This assumes that your suite classes end in "Tests" ("FastTests", "SmokeTests", etc) while your other tests end in something else ("ServerTest", etc)

kcooney avatar Jun 20 '11 17:06 kcooney

Yes, we want to use categories. We also have a load of legacy tests that we don't want to rename. I'm aware of ClasspathSuite, but it really didn't meet our news for those reasons.

mattinger avatar Jun 20 '11 18:06 mattinger

I have tried your solution and found that it would probably work for us:

@ClassnameFilters({".*Test"}) @IncludeJars(false) @RunWith(ClasspathSuite.class) public class AllClassesSuite {

}

However, the one issue I saw was where the test suite classes lived. I wanted to make a seperate module to define the different categories:

@Category(FastTestCategory.class) @Category(SlowTestCategory.class)

And then create a shared test suite for running them:

@RunWith(Categories.class) @IncludeCategory(FastTestCategory.class) @SuiteClasses({AllClassesSuite.class}) public class FastTestSuite {

}

The problem I encountered was that if FastTestSuite was brought into the module via a maven dependency, and then my individual tests were in the current module, no tests were run when I tried to execute that suite.

I'm not sure if this is a classloading issue or just a maven issue. If i try to run any suite not immediately in the current module, no tests get executed. But if i move that suite to the current module it's fine.

mattinger avatar Jun 20 '11 20:06 mattinger

I suggest asking on the mailing list: http://tech.groups.yahoo.com/group/junit/

My guess is if you have tests in a different module, then they will be in a jar, so will get filtered out by @IncludeJars(false), but that's just a guess.

The general idea is you can use a reflection-based suite builder to find all of your tests, and then use Categories to select which ones to run.

kcooney avatar Jun 20 '11 20:06 kcooney

I tried changing includeJars to true, with no luck.

I think the problem is that the suite is in a jar file, and the tests are not. So the surefire plugin is getting confused due to it's classloader. It works fine in eclipse.

mattinger avatar Jun 20 '11 20:06 mattinger

I was hoping to have the suite in a single place, the groups support in junit is kind of tacked on after the fact, and isn't truly native to junit. I'd love just to be able to say: mvn test -Djunit.groups=FastTest

and be done with it, but alas, i have to create classes to represent the stuff, and create a suite to run it. As a result, in order to be able to just run the FastGroup group for a multi module project, I have to replicate the same suite in every module, and it would have to have the exact same classname. Then i run:

mvn test -Dtest=com.myorg.FastTest

Just a maintenance headache.

mattinger avatar Jun 20 '11 20:06 mattinger

These sound like mvn issues. Have you tried asking on the mailing list?

kcooney avatar Jun 21 '11 21:06 kcooney

Hi all,

i created an issue with an explanation and a solution. i suggest the JUnit team to request the source code in order to open a discussion and integrate my changes by them or me (does not really matter who will make the submit).

https://github.com/KentBeck/junit/issues/307

@mattinger Please have a look in to the example. You may comment it anyway if it would solve your problem.

Tibor17 avatar Sep 12 '11 18:09 Tibor17

@mattinger, some things are getting fixed with respect to Category support, although we still don't have an internal answer to ClasspathSuite. Is this a continuing issue for you?

dsaff avatar Jun 27 '13 19:06 dsaff

Hello,

I too would very much like some automated discovery of categories of some sort in the core JUnit framework.

Other solutions such as the wildcard based classpath scanning are available, but none seem to be part of JUnit itself. I would also prefer a solution that uses annotation metadata (such as these categories), or maybe both would be interesting.

I came across this issue while examining the JUnit categories but I find it inconvenient that one must always enumerate each class using the @SuiteClasses annotation, because this is easily forgotten.

kvanrobbroeck avatar Jul 30 '14 14:07 kvanrobbroeck

I've started working on integrating the best parts from @dhemery's runtime-suite and @jlink's ClasspathSuite a while ago. Maybe that would be something for 4.13?

marcphilipp avatar Jul 31 '14 19:07 marcphilipp

@marcphilipp, that could certainly be useful.

dsaff avatar Jul 31 '14 19:07 dsaff

@Marc Need help? We could do a session at Socrates... Johannes

Von meinem iPad gesendet

Am 31.07.2014 um 21:22 schrieb Marc Philipp [email protected]:

I've started working on integrating the best parts from @dhemery's runtime-suite and @jlink's ClasspathSuite a while ago. Maybe that would be something for 4.13?

— Reply to this email directly or view it on GitHub.

jlink avatar Jul 31 '14 19:07 jlink

I regret that I don't have the energy to participate in this right now. But I'll be following along.

dhemery avatar Jul 31 '14 21:07 dhemery

@jlink We can take a look at it there for sure. Now I just have to dig through my workspaces and find it… ;-)

marcphilipp avatar Aug 01 '14 06:08 marcphilipp

JUnit 4 is now in maintenance mode.

At this point, only critical bugs and security issues will be fixed.

The team has therefore decided to close this issue.

junit-builds avatar May 30 '25 09:05 junit-builds