Недавно наш проект стал переходить с 6 явы на 7, и в процессе возникало множество различных проблем.
Одна из них - беспорядочный порядок выполнения тестов четвёртым junit'ом.
Вообще говоря, порядок выполнения тестов может быть любым.
Каждый тест должен быть независимым от всех остальных. Так же вы можете это прочесть в документации junit'a. Но если у вас уже есть тысячи тестов и они "падают" в случае если их запускать в неопределённом порядке - это большая проблема и вы потратите кучу времени на рефакторинг всех тестов, делая их независимыми друг от друга. Я хочу предложить вам своё временное решение данной проблемы.
Junit использует reflection для получения и запуска тестовых методов. Для получения списка методов которые содеражтся в тестовом классе используется метод "Method[] getDeclaredMethods()" содержащийся в классе java.lang.Class.
Вы можете прочесть в документации к этому методу, либо в документации самого junit'a что:
"The elements in the array returned are not sorted and are not in any particular order." (Элементы в возвращаемом массиве никак не отсоритрованны и никак не упорядоченны), но в предыдущих реализациях ява машины возвращаемый список методов, был отсортирован в том порядке, в котором эти методы были в коде. И многие этим пользовались игнорируя документацию. В результате часть написанных тестов зависят друг от друга. Изначально было неправильно полагаться на порядок тестов, но что сделано, то сделано, и теперь у нас есть проблема требующая быстрого (временного) решения.
Мы будем сортировать список тестов прямо перед тем как junit начнёт их запускать. Мы изменим некоторые классы junit'a, скомпилируем их и поместим перед junit.jar в наш classpath, тем самым мы подменим оригинальные классы, без нужды пересобирать сам junit. Это может быть полезно, если вам проще добавить новую зависимость к проекту, чем изменять стороннюю библиотеку.
Junit 4 имеет два разных базовых класса отвечающих за запуск тестов: JUnit3Builder и JUnit4Builder из пакета org.junit.internal.builders. JUnit3Builder используется для запуска тестов старого формата (без использования аннотаций и прочего).
Мы переопределим их следующим образом:
package org.junit.internal.builders;
import custom.junit.runners.OrderedJUnit3ClassRunner;
import org.junit.internal.runners.JUnit38ClassRunner;
import org.junit.runner.Runner;
import org.junit.runners.model.RunnerBuilder;
public class JUnit3Builder extends RunnerBuilder {
public Runner runnerForClass(Class testClass) throws Throwable {
if (isPre4Test(testClass)) {
// return new JUnit38ClassRunner(testClass);
return new OrderedJUnit3ClassRunner(testClass);
} else {
return null;
}
}
boolean isPre4Test(Class testClass) {
return junit.framework.TestCase.class.isAssignableFrom(testClass);
}
}
package org.junit.internal.builders;
import custom.junit.runners.OrderedJUnit4ClassRunner;
import org.junit.runner.Runner;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.RunnerBuilder;
/**
* This must come first on the classpath before JUnit 4's jar so it
* is instantiated instead of the default JUnit 4 builder.
*/
public class JUnit4Builder extends RunnerBuilder {
@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
// Using Class Runner with sorting of test methods:
// sorting in order as they are in java code.
return new OrderedJUnit4ClassRunner(testClass);
// JUnit Original Class Runner
// return new BlockJUnit4ClassRunner(testClass);
}
}
Для junit 3 нам потребуется так же собственная реализация класса TestSuite. Когда где-либо в ядре junit coreбудет добавлен новый тестовый метод в список тестов, он попадёт в наш отсортированный список - OrderedTestSuite.
package custom.junit.runners;
import custom.junit.framework.OrderedTestSuite;
import junit.framework.Test;
import junit.framework.TestCase;
import org.apache.log4j.Logger;
import org.junit.internal.runners.JUnit38ClassRunner;
public class OrderedJUnit3ClassRunner extends JUnit38ClassRunner {
private static final Logger logger = Logger.getLogger(OrderedJUnit3ClassRunner.class.getName());
public OrderedJUnit3ClassRunner(Class<?> aClass) {
this(new OrderedTestSuite(aClass.asSubclass(TestCase.class)));
}
public OrderedJUnit3ClassRunner(Test test) {
super(test);
logger.info("Using custom JUNIT CLASS RUNNER: " + this.getClass().getCanonicalName());
}
}
package custom.junit.framework;
import custom.junit.runners.MethodComparator;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.apache.log4j.Logger;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class OrderedTestSuite extends TestSuite {
private static final Logger logger = Logger.getLogger(OrderedTestSuite.class.getName());
public OrderedTestSuite(final Class<?> theClass) {
addTestsFromTestCase(theClass);
}
/**
* Adds the tests from the given class to the suite
*/
@Override
public void addTestSuite(Class<? extends TestCase> testClass) {
addTest(new OrderedTestSuite(testClass));
}
private void addTestsFromTestCase(final Class<?> theClass) {
this.setName(theClass.getName());
try {
getTestConstructor(theClass); // Avoid generating multiple error messages
} catch (NoSuchMethodException e) {
addTest(warning("Class " + theClass.getName() + " has no public constructor TestCase(String name) or TestCase()"));
return;
}
if (!Modifier.isPublic(theClass.getModifiers())) {
addTest(warning("Class " + theClass.getName() + " is not public"));
return;
}
Class<?> superClass = theClass;
List<String> names = new ArrayList<String>();
while (Test.class.isAssignableFrom(superClass)) {
Method[] methods = superClass.getDeclaredMethods();
// Sorting methods.
final List<Method> methodList = new ArrayList<Method>(Arrays.asList(methods));
try {
Collections.sort(methodList, MethodComparator.getMethodComparatorForJUnit3());
methods = methodList.toArray(new Method[methodList.size()]);
} catch (Throwable throwable) {
logger.fatal("addTestsFromTestCase(): Error while sorting test cases! Using default order (random).", throwable);
}
for (Method each : methods) {
addTestMethod(each, names, theClass);
}
superClass = superClass.getSuperclass();
}
if (this.testCount() == 0)
addTest(warning("No tests found in " + theClass.getName()));
}
private void addTestMethod(Method m, List<String> names, Class<?> theClass) {
String name = m.getName();
if (names.contains(name))
return;
if (!isPublicTestMethod(m)) {
if (isTestMethod(m))
addTest(warning("Test method isn't public: " + m.getName() + "(" + theClass.getCanonicalName() + ")"));
return;
}
names.add(name);
addTest(createTest(theClass, name));
}
private boolean isPublicTestMethod(Method m) {
return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
}
private boolean isTestMethod(Method m) {
return m.getParameterTypes().length == 0 && m.getName().startsWith("test") && m.getReturnType().equals(Void.TYPE);
}
}
Для junit 4, всё намного проще, в нём имеется специальный метод отвечающий за получения списка тестов, поэтому нам достаточно перекрыть лишь его we can override just one special method - "protected List<FrameworkMethod> computeTestMethods()".
package custom.junit.runners;
import org.apache.log4j.Logger;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class OrderedJUnit4ClassRunner extends BlockJUnit4ClassRunner {
private static final Logger logger = Logger.getLogger(OrderedJUnit4ClassRunner.class.getName());
public OrderedJUnit4ClassRunner(Class aClass) throws InitializationError {
super(aClass);
logger.info("Using custom JUNIT CLASS RUNNER: " + this.getClass().getCanonicalName());
}
@Override
protected List<FrameworkMethod> computeTestMethods() {
final List<FrameworkMethod> list = super.computeTestMethods();
try {
final List<FrameworkMethod> copy = new ArrayList<FrameworkMethod>(list);
Collections.sort(copy, MethodComparator.getFrameworkMethodComparatorForJUnit4());
return copy;
} catch (Throwable throwable) {
logger.fatal("computeTestMethods(): Error while sorting test cases! Using default order (random).", throwable);
return list;
}
}
}
И последнее - нам нужен сам механизм сравнения с помощью которого мы сможем упорядочить тесты так, как они находятся в исходном коде. Для этого будем использовать специальный класс типа компаратор. Конечно же вы можете реализовать свой собственный компаратор, который бы сортировал тесты в любом другом, нужном вам порядке. Мой компаратор сортирует тесты в том порядке, в котором они находятся в исходном коде, или точнее в байт-коде.
package custom.junit.runners;
import org.apache.log4j.Logger;
import org.junit.Ignore;
import org.junit.runners.model.FrameworkMethod;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.lang.reflect.Method;
import java.util.Comparator;
public class MethodComparator<T> implements Comparator<T> {
private static final Logger logger = Logger.getLogger(MethodComparator.class.getName());
private static final char[] METHOD_SEPARATORS = {1, 7};
private MethodComparator() {
}
public static MethodComparator<FrameworkMethod> getFrameworkMethodComparatorForJUnit4() {
return new MethodComparator<FrameworkMethod>();
}
public static MethodComparator<Method> getMethodComparatorForJUnit3() {
return new MethodComparator<Method>();
}
@Override
public int compare(T o1, T o2) {
final MethodPosition methodPosition1 = this.getIndexOfMethodPosition(o1);
final MethodPosition methodPosition2 = this.getIndexOfMethodPosition(o2);
return methodPosition1.compareTo(methodPosition2);
}
private MethodPosition getIndexOfMethodPosition(final Object method) {
if (method instanceof FrameworkMethod) {
return this.getIndexOfMethodPosition((FrameworkMethod) method);
} else if (method instanceof Method) {
return this.getIndexOfMethodPosition((Method) method);
} else {
logger.error("getIndexOfMethodPosition(): Given object is not a method! Object class is "
+ method.getClass().getCanonicalName());
return new NullMethodPosition();
}
}
private MethodPosition getIndexOfMethodPosition(final FrameworkMethod frameworkMethod) {
return getIndexOfMethodPosition(frameworkMethod.getMethod());
}
private MethodPosition getIndexOfMethodPosition(final Method method) {
final Class aClass = method.getDeclaringClass();
if (method.getAnnotation(Ignore.class) == null) {
return getIndexOfMethodPosition(aClass, method.getName());
} else {
logger.debug("getIndexOfMethodPosition(): Method is annotated as Ignored: " + method.getName()
+ " in class: " + aClass.getCanonicalName());
return new NullMethodPosition();
}
}
private MethodPosition getIndexOfMethodPosition(final Class aClass, final String methodName) {
MethodPosition methodPosition;
for (final char methodSeparator : METHOD_SEPARATORS) {
methodPosition = getIndexOfMethodPosition(aClass, methodName, methodSeparator);
if (methodPosition instanceof NullMethodPosition) {
logger.debug("getIndexOfMethodPosition(): Trying to use another method separator for method: " + methodName);
} else {
return methodPosition;
}
}
return new NullMethodPosition();
}
private MethodPosition getIndexOfMethodPosition(final Class aClass, final String methodName, final char methodSeparator) {
final InputStream inputStream = aClass.getResourceAsStream(aClass.getSimpleName() + ".class");
final LineNumberReader lineNumberReader = new LineNumberReader(new InputStreamReader(inputStream));
final String methodNameWithSeparator = methodName + methodSeparator;
try {
try {
String line;
while ((line = lineNumberReader.readLine()) != null) {
if (line.contains(methodNameWithSeparator)) {
return new MethodPosition(lineNumberReader.getLineNumber(), line.indexOf(methodNameWithSeparator));
}
}
} finally {
lineNumberReader.close();
}
} catch (IOException e) {
logger.error("getIndexOfMethodPosition(): Error while reading byte code of class " + aClass.getCanonicalName(), e);
return new NullMethodPosition();
}
logger.warn("getIndexOfMethodPosition(): Can't find method " + methodName + " in byte code of class " + aClass.getCanonicalName());
return new NullMethodPosition();
}
private static class MethodPosition implements Comparable<MethodPosition> {
private final Integer lineNumber;
private final Integer indexInLine;
public MethodPosition(int lineNumber, int indexInLine) {
this.lineNumber = lineNumber;
this.indexInLine = indexInLine;
}
@Override
public int compareTo(MethodPosition o) {
// If line numbers are equal, then compare by indexes in this line.
if (this.lineNumber.equals(o.lineNumber)) {
return this.indexInLine.compareTo(o.indexInLine);
} else {
return this.lineNumber.compareTo(o.lineNumber);
}
}
}
private static class NullMethodPosition extends MethodPosition {
public NullMethodPosition() {
super(-1, -1);
}
}
}
Вы можете использовать различные продвинутые библиотеки для декомпиляции байт-кода, например ASM, а я просто ищу в байт коде символы-разделители, которые идут сразу за именем метода.
Надеюсь этот пост будет вам полезен.
Любые комментарии приветствуются.
ВЫ ПРОСТО ЦАРЬ!
ОтветитьУдалитьДА! Я БЕЗУМНО КРУТ 8-)
Удалить