我要解决什么问题
几个月前,我们开始在项目中使用JMH来测试和发现绩效问题。
该工具提供了多种模式和参考器,我们发现这对我们的目的有用。
我正在使用的Intellij Idea具有有用的Intellij IDEA plugin for Java Microbenchmark Harness。该插件的功能类似于Junit插件。它通过允许一起运行一些基准或单独运行一些基准来简化基准开发和调试。
但是...我们的一半团队使用Eclipse作为主要IDE,IDE没有任何插件或对该工具的支持。
即使我们可以使用main
方法运行它,更改include
模式也不忘记将更改恢复为git。
因此,在小头脑风暴之后,我们决定编写一个具有功能的自定义Junit Runner以运行基准。
Junit 4跑者
Junit 4具有API,可以将任何类作为测试套件。
您只需要:
- 写入kude2 class的课程
public class BenchmarkRunner extends Runner {
//...
}
- 实现构造函数和几种方法
public class BenchmarkRunner extends Runner {
public BenchmarkRunner(Class<?> benchmarkClass) {
}
public Description getDescription() {
//...
}
public void run(RunNotifier notifier) {
//...
}
}
- 将跑步者添加到您的测试课上
@RunWith(BenchmarkRunner.class)
public class CustomCollectionBenchmark {
//...
}
实施基准Junit Runner
首先,我们需要向Junit Engine提供有关测试的信息。
Junit跑步者具有getDescription()
方法。但是如何获取有关测试类和测试方法的信息?
来自Javadoc:
创建自定义跑步者时,除了在此处实施抽象方法外,您还必须提供一个构造函数,该构造函数将作为参数包含测试的类别。
因此,我们可能会将课程作为构造函数参数,并在reflection的帮助下获取所有信息。
public class BenchmarkRunner extends Runner {
private final Class<?> benchmarkClass; // (1)
private final List<Method> benchmarkMethods; // (2)
public BenchmarkRunner(Class<?> benchmarkClass) {
this.benchmarkClass = benchmarkClass;
this.benchmarkMethods = Arrays.stream(benchmarkClass.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Benchmark.class))
.collect(Collectors.toList());
}
//...
}
现在,我拥有所有必需的信息,可以为Junit引擎提供测试套件描述:
- 实际基准类
- 此类标记为
@Benchmark
标记的所有方法
public class BenchmarkRunner extends Runner {
//...
@Override
public Description getDescription() {
Description result = Description.createSuiteDescription(benchmarkClass);
benchmarkMethods.stream()
.map(m -> Description.createTestDescription(benchmarkClass, m.getName()))
.forEach(result::addChild);
return result;
}
//...
}
运行测试后,我们可能会看到这样的东西:
让我们运行基准。
我们需要实现org.junit.runner.Runner.run(RunNotifier)
的方法,其中RunNotifier
负责通知引擎有关测试运行。
这个想法是我们依次运行一对一的基准方法,每个方法都在单独的org.openjdk.jmh.runner.Runner
中。
public class BenchmarkRunner extends Runner {
...
@Override
public void run(RunNotifier notifier) {
for (Method benchmarkMethod : benchmarkMethods) {
Description testDescription = getBenchmarkMethodDescription(benchmarkMethod);
try {
notifier.fireTestStarted(testDescription);
Options opt = new OptionsBuilder()
.include(".*" + benchmarkClass.getName() + "." + benchmarkMethod.getName() + ".*")
.jvmArgsAppend("-Djmh.separateClasspathJAR=true")
.build();
new org.openjdk.jmh.runner.Runner(opt).run();
notifier.fireTestFinished(testDescription);
} catch (Exception e) {
notifier.fireTestFailure(new Failure(testDescription, e));
return;
}
}
}
private Description getBenchmarkMethodDescription(Method benchmarkMethod) {
return Description.createTestDescription(benchmarkClass, benchmarkMethod.getName());
}
}
选项的意思是:
-
include
-我们想在运行中包括的基准。 -
jvmArgsAppend("-Djmh.separateClasspathJAR=true")
-特定选项,告诉JMH构建classpath.jar
以避免The filename or extension is too long错误
您可能会看到,我们在开始和完成测试(成功与否)时通知RunNotifier
。
看起来不错,但是即使选择仅运行一个测试,我们也在运行所有测试。
过滤
我们的跑步者应实现org.junit.runner.manipulation.Filterable
接口,以允许Junit Engine告诉我们的代码,应该运行哪些测试。
该界面只有单个方法void filter(Filter)
和org.junit.runner.manipulation.Filter
参数具有我们可以使用的shouldRun(Description)
方法,我们可以知道是否要求运行测试。
该方法没有返回任何内容,因此看起来我们需要存储过滤结果并以后使用它们。
public class BenchmarkRunner extends Runner implements Filterable {
//...
private List<Method> readyToRunMethods; // <= add new field to store filter result
@Override
public Description getDescription() {
Description result = Description.createSuiteDescription(benchmarkClass);
readyToRunMethods.stream() // <= use the field here
.map(this::getBenchmarkMethodDescription)
.forEach(result::addChild);
return result;
}
//...
@Override
public void run(RunNotifier notifier) {
for (Method benchmarkMethod : readyToRunMethods) { // <= and here
//...
}
}
@Override
public void filter(Filter filter) throws NoTestsRemainException {
List<Method> filteredMethods = new ArrayList<>();
for (Method benchmarkMethod : benchmarkMethods) {
if (filter.shouldRun(getBenchmarkMethodDescription(benchmarkMethod))) {
filteredMethods.add(benchmarkMethod);
}
}
if (filteredMethods.isEmpty()) {
throw new NoTestsRemainException();
}
this.readyToRunMethods = filteredMethods;
}
}
现在它只运行我们要求运行的方法。
结论
最后,在开发和调试期间,我们有了统一的方法来从任何IDE运行基准。这确实简化了我们的日常生活。
我们仍在使用main
方法运行基准测试,以减少环境噪声并获得可靠的结果进行分析。
您可以在GitHub中找到代码。