计算@Category在JUnit中的一组测试中出现的次数 [英] Get a count for the number of times a @Category appears in a suite of tests in JUnit
问题描述
我已经使用Java,Selenium,Junit,Maven开发了一套完整的自动化测试.
I have developed a full suite of automated tests using Java, Selenium, Junit, Maven.
对于每个测试,它们都有一个或多个@Category注释,用于描述每个测试涵盖的软件区域.例如:
For each test, they have one or more @Category annotations describing what area of the software each test covers. For instance:
@Test
@Category({com.example.core.categories.Priority1.class,
com.example.core.categories.Export.class,
com.example.core.categories.MemberData.class})
@Test
@Category({com.example.core.categories.Priority1.class,
com.example.core.categories.Import.class,
com.example.core.categories.MemberData.class})
@Test
@Ignore
@Category({com.example.core.categories.Priority2.class,
com.example.core.categories.Import.class,
com.example.core.categories.MemberData.class})
我想做的是找到一种方法来计算包含给定类别的测试数量.所有可能的类别都是//com/example/core/categories
文件夹中的文件名,作为源列表.
What I'm trying to do is find a way to get a count of how many tests contain any given category. All the possible categories are filenames in the //com/example/core/categories
folder as a source list.
我曾尝试构建一个shell脚本来进行字数统计,这似乎还可以,但是我认为还可以使用更多内置"来处理@Category.
I've tried building a shell script to do a word count, which seems to work okay, but I would think there would be something more "built-in" to deal with @Category.
我最大的问题是,即使我得到了正确的计数,也很有可能将一个或多个测试标记为@Ignore,这应该使该测试@Category无效,但无需大量使用标志并读取每个文件行-按顺序排列,以排除正确的计数.
My biggest issue is that even if I get the right count, it is very possible that one or more of the tests are marked @Ignore which should nullify that tests @Category's but without heavy use of flags and reading every file line-by-line in order it throws off the correct count.
是否有一种很好的方法可以逐项列出@Category的内容,而这也是@Ignore的因素?
Is there a good way to itemize @Category's that also factors in @Ignore?
示例输出
| Category | Count |
|----------------------------------------------|------:|
| com.example.core.categories.Export.class | 1 |
| com.example.core.categories.Import.class | 1 |
| com.example.core.categories.MemberData.class | 2 |
| com.example.core.categories.Priority1.class | 2 |
| com.example.core.categories.Priority2.class | 0 |
| com.example.core.categories.Priority3.class | 0 |
推荐答案
动态按类别进行的测试"计算机
(推荐方法)
Dynamic 'Tests by Category' Computer
(Recommended method)
我尝试了一种使用抽象层中的计数器执行此操作的方法,但是这很痛苦,必须在每个Test方法的开头添加源代码.
I tried a way to perform this with a counter in abstract layer but it was painful, having to add source code at beginning of each Test methods.
最后,这是我为满足您的需求而编写的源代码.它相当沉重(反射...),但是它对现有源代码的干扰较小,并且完全可以满足您的需求.
At end, this is the source code I wrote to answer your needs; it is quite heavy (reflection ...), but it is the less intrusive with existing source code, and answers totally to your needs.
首先,您必须创建一个Testsuite
(包含其他各种套件,或直接包含您想要的所有Test类),以确保最后已经加载了您想要统计的所有Test.
First, you must create a Testsuite
(containing various others Suites, or directly all the Test classes you want), to ensure at end, that all Tests for which you want statistics, have been loaded.
在此套件中,您必须实现一个名为@AfterClass
的最终挂钩",当整个测试套件已由 JUnit 完全管理时,它将被一次调用.
In this Suite, you have to implement a "final Hook", called @AfterClass
which will be called once for all, when the whole Test suite has been fully managed by JUnit.
这是我为您编写的测试套件实现:
This the the Test Suite implementation I wrote for you:
package misc.category;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.AfterClass;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({ UnitTestWithCategory.class })
public class TestSuiteCountComputer {
public static final String MAIN_TEST_PACKAGES = "misc.category";
private static final Class<?>[] getClasses(final ClassLoader classLoader)
throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
Class<?> CL_class = classLoader.getClass();
while (CL_class != java.lang.ClassLoader.class) {
CL_class = CL_class.getSuperclass();
}
java.lang.reflect.Field ClassLoader_classes_field = CL_class.getDeclaredField("classes");
ClassLoader_classes_field.setAccessible(true);
Vector<?> classVector = (Vector<?>) ClassLoader_classes_field.get(classLoader);
Class<?>[] classes = new Class[classVector.size()]; // Creates an array to avoid concurrent modification
// exception.
return classVector.toArray(classes);
}
// Registers the information.
private static final void registerTest(Map<String, AtomicInteger> testByCategoryMap, String category) {
AtomicInteger count;
if (testByCategoryMap.containsKey(category)) {
count = testByCategoryMap.get(category);
} else {
count = new AtomicInteger(0);
testByCategoryMap.put(category, count);
}
count.incrementAndGet();
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
Map<String, AtomicInteger> testByCategoryMap = new HashMap<>();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
while (classLoader != null) {
for (Class<?> classToCheck : getClasses(classLoader)) {
String packageName = classToCheck.getPackage() != null ? classToCheck.getPackage().getName() : "";
if (!packageName.startsWith(MAIN_TEST_PACKAGES))
continue;
// For each methods of the class.
for (Method method : classToCheck.getDeclaredMethods()) {
Class<?>[] categoryClassToRegister = null;
boolean ignored = false;
for (Annotation annotation : method.getAnnotations()) {
if (annotation instanceof org.junit.experimental.categories.Category) {
categoryClassToRegister = ((org.junit.experimental.categories.Category) annotation).value();
} else if (annotation instanceof org.junit.Ignore) {
ignored = true;
} else {
// Ignore this annotation.
continue;
}
}
if (ignored) {
// If you want to compute count of ignored test.
registerTest(testByCategoryMap, "(Ignored Tests)");
} else if (categoryClassToRegister != null) {
for (Class<?> categoryClass : categoryClassToRegister) {
registerTest(testByCategoryMap, categoryClass.getCanonicalName());
}
}
}
}
classLoader = classLoader.getParent();
}
System.out.println("\nFinal Statistics:");
System.out.println("Count of Tests\t\tCategory");
for (Entry<String, AtomicInteger> info : testByCategoryMap.entrySet()) {
System.out.println("\t" + info.getValue() + "\t\t" + info.getKey());
}
}
}
您可以适应您的需求,尤其是我一开始创建的常量,以过滤要考虑的软件包.
You can adapt to your needs, in particular the constant I created at beginning, to filter package to consider.
然后,您无所事事.
例如,这是我很小的测试类:
For instance, this is my tiny Test Class:
package misc.category;
import org.junit.Test;
import org.junit.experimental.categories.Category;
public class UnitTestWithCategory {
@Category({CategoryA.class, CategoryB.class})
@Test
public final void Test() {
System.out.println("In Test 1");
}
@Category(CategoryA.class)
@Test
public final void Test2() {
System.out.println("In Test 2");
}
}
在这种情况下,输出为:
In this case, the output is:
In Test 1
In Test 2
Final Statistics:
Count of Tests Category
1 misc.category.CategoryB
2 misc.category.CategoryA
并带有包含@Ignore
批注的测试用例:
And with Test case containing @Ignore
annotation:
package misc.category;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;
public class UnitTestWithCategory {
@Category({CategoryA.class, CategoryB.class})
@Test
public final void Test() {
System.out.println("In Test 1");
}
@Category(CategoryA.class)
@Test
public final void Test2() {
System.out.println("In Test 2");
}
@Category(CategoryA.class)
@Ignore
@Test
public final void Test3() {
System.out.println("In Test 3");
}
}
您将获得输出:
In Test 1
In Test 2
Final Statistics:
Count of Tests Category
1 (Ignored Tests)
1 misc.category.CategoryB
2 misc.category.CategoryA
如果需要,您可以轻松删除(已忽略的测试)"注册,并且当然可以根据需要调整输出.
You can easily remove the "(Ignored Tests)" registration if you want, and of course adapt the output as you want.
此最终版本非常不错的一点是,它将处理已真正加载/执行的测试类,因此您将获得有关已执行内容的真实统计信息,而不是像您这样的静态统计信息到目前为止.
What is very nice with this final version, is that it will take care of Test Classes which have really been loaded/executed, and so you will have a real statistics of what have been executed, instead of a static statistics like you got so far.
如果您希望像您所要求的那样与现有源代码无关,那么这是一种静态执行 Tests by Category 计算的方法.
If you want, like you asked, to have nothing to do on existing source code, this is a way to perform the Tests by Category computation statically.
这是我为您写的StaticTestWithCategoryCounter
:
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;
public class StaticTestWithCategoryCounter {
public static final String ROOT_DIR_TO_SCAN = "bin";
public static final String MAIN_TEST_PACKAGES = "misc.category";
private static final Class<?>[] getClasses(final ClassLoader classLoader)
throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
Class<?> CL_class = classLoader.getClass();
while (CL_class != java.lang.ClassLoader.class) {
CL_class = CL_class.getSuperclass();
}
java.lang.reflect.Field ClassLoader_classes_field = CL_class.getDeclaredField("classes");
ClassLoader_classes_field.setAccessible(true);
Vector<?> classVector = (Vector<?>) ClassLoader_classes_field.get(classLoader);
Class<?>[] classes = new Class[classVector.size()]; // Creates an array to avoid concurrent modification
// exception.
return classVector.toArray(classes);
}
// Registers the information.
private static final void registerTest(Map<String, AtomicInteger> testByCategoryMap, String category) {
AtomicInteger count;
if (testByCategoryMap.containsKey(category)) {
count = testByCategoryMap.get(category);
} else {
count = new AtomicInteger(0);
testByCategoryMap.put(category, count);
}
count.incrementAndGet();
}
public static void computeCategoryCounters() throws Exception {
Map<String, AtomicInteger> testByCategoryMap = new HashMap<>();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
while (classLoader != null) {
for (Class<?> classToCheck : getClasses(classLoader)) {
String packageName = classToCheck.getPackage() != null ? classToCheck.getPackage().getName() : "";
if (!packageName.startsWith(MAIN_TEST_PACKAGES))
continue;
// For each methods of the class.
for (Method method : classToCheck.getDeclaredMethods()) {
Class<?>[] categoryClassToRegister = null;
boolean ignored = false;
for (Annotation annotation : method.getAnnotations()) {
if (annotation instanceof org.junit.experimental.categories.Category) {
categoryClassToRegister = ((org.junit.experimental.categories.Category) annotation).value();
} else if (annotation instanceof org.junit.Ignore) {
ignored = true;
} else {
// Ignore this annotation.
continue;
}
}
if (ignored) {
// If you want to compute count of ignored test.
registerTest(testByCategoryMap, "(Ignored Tests)");
} else if (categoryClassToRegister != null) {
for (Class<?> categoryClass : categoryClassToRegister) {
registerTest(testByCategoryMap, categoryClass.getCanonicalName());
}
}
}
}
classLoader = classLoader.getParent();
}
System.out.println("\nFinal Statistics:");
System.out.println("Count of Tests\t\tCategory");
for (Entry<String, AtomicInteger> info : testByCategoryMap.entrySet()) {
System.out.println("\t" + info.getValue() + "\t\t" + info.getKey());
}
}
public static List<String> listNameOfAvailableClasses(String rootDirectory, File directory, String packageName) throws ClassNotFoundException {
List<String> classeNameList = new ArrayList<>();
if (!directory.exists()) {
return classeNameList;
}
File[] files = directory.listFiles();
for (File file : files) {
if (file.isDirectory()) {
if (file.getName().contains("."))
continue;
classeNameList.addAll(listNameOfAvailableClasses(rootDirectory, file, packageName));
} else if (file.getName().endsWith(".class")) {
String qualifiedName = file.getPath().substring(rootDirectory.length() + 1);
qualifiedName = qualifiedName.substring(0, qualifiedName.length() - 6).replaceAll(File.separator, ".");
if (packageName ==null || qualifiedName.startsWith(packageName))
classeNameList.add(qualifiedName);
}
}
return classeNameList;
}
public static List<Class<?>> loadAllAvailableClasses(String rootDirectory, String packageName) throws ClassNotFoundException {
List<String> classeNameList = listNameOfAvailableClasses(rootDirectory, new File(rootDirectory), packageName);
List<Class<?>> classes = new ArrayList<>();
for (final String className: classeNameList) {
classes.add(Class.forName(className));
}
return classes;
}
public static void main(String[] args) {
try {
loadAllAvailableClasses(ROOT_DIR_TO_SCAN, MAIN_TEST_PACKAGES);
computeCategoryCounters();
} catch (Exception e) {
e.printStackTrace();
}
}
}
您只需要在开始时修改两个常量即可指定:
You just need to adapt the two constants at beginning to specify:
- (字节码)类在哪里
- 您对哪个主软件包感兴趣(可以将其设置为
null
以考虑100%可用的软件包)
- where are the (bytecode) classes
- which main package is interesting you (can you set it to
null
to regard 100% available packages)
这个新版本的想法:
- 列出与您的2个常量匹配的所有类文件
- 加载所有对应的类
- 使用未经修改的动态版本源代码(现在已经加载了类)
如果您需要更多信息,请告诉我.
Let me know if you need further information.
这篇关于计算@Category在JUnit中的一组测试中出现的次数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!