前言
上一节介绍了 https://github.com/houbb/junitperf 的入门使用。
这一节我们从源码的角度,剖析一下其实现方式。
性能测试该怎么做?
Junit Rules
junit4 小伙伴们肯定不陌生,那么 junit rules 你听过说过吗?
要想基于 junit4 实现一个性能测试框架,最核心的一点在于理解 Junit Rules。
官方文档:https://github.com/junit-team/junit4/wiki/Rules
Rules 作用
规则允许非常灵活地添加或重新定义测试类中每个测试方法的行为。
测试人员可以重用或扩展下面提供的规则之一,或者编写自己的规则。
自定义规则
ps: 下面的内容来自官方的例子。
大多数自定义规则可以作为 ExternalResource 规则的扩展来实现。
但是,如果您需要有关所讨论的测试类或方法的更多信息,则需要实现 TestRule 接口。
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;public class IdentityRule implements TestRule {
@Overridepublic Statement apply(final Statement base, final Description description) {
return base;}
}
当然,实现 TestRule 的强大功能来自使用自定义构造函数的组合、向类添加方法以用于测试,以及将提供的 Statement 包装在新的 Statement 中。
例如,考虑以下为每个测试提供命名记录器的测试规则:
package org.example.junit;import java.util.logging.Logger;import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;public class TestLogger implements TestRule {
private Logger logger;public Logger getLogger() {
return this.logger;}@Overridepublic Statement apply(final Statement base, final Description description) {
return new Statement() {
@Overridepublic void evaluate() throws Throwable {
logger = Logger.getLogger(description.getTestClass().getName() + '.' + description.getDisplayName());base.evaluate();}};}
}
然后这个规则就可以按照下面的方式使用:
import java.util.logging.Logger;import org.example.junit.TestLogger;
import org.junit.Rule;
import org.junit.Test;public class MyLoggerTest {
@Rulepublic final TestLogger logger = new TestLogger();@Testpublic void checkOutMyLogger() {
final Logger log = logger.getLogger();log.warn("Your test is showing!");}}
定义和使用
看了上面的例子,我们发现 junit4 中的自定义规则还是比较简单的。
定义方式:实现 TestRule 接口
使用方式;使用 @Rule
放在创建的内部属性上。
是不是很简单呢?
好了你已经学会 1+1=2 了,下面让我们来学习一下泰勒展开吧。
性能测试算法流程
如何统计一个方法的执行耗时呢?
相信你一定不会陌生,只需要在方法执行开始前和结束后各统计一个时间,然后差值就是耗时。
如何模拟多个线程调用呢?
使用 java 多线程执行进行模拟即可。
如何生成报告文件呢?
把上述统计的各个维度数据,结合生成对应的 html 等文件即可。
我们将要做的事情,就是把上面的点综合起来,然后结合 Junit4 Rules 实现即可。
听起来也不难不是吗?
下面,让我们来一起看一看实现源码吧。
Rule 的入门
入门例子
我们首先看一个 junit4 的入门例子:
public class HelloWorldTest {
@Rulepublic JunitPerfRule junitPerfRule = new JunitPerfRule();/*** 单一线程,执行 1000ms,默认以 html 输出测试结果* @throws InterruptedException if any*/@Test@JunitPerfConfig(duration = 1000)public void helloWorldTest() throws InterruptedException {
System.out.println("hello world");Thread.sleep(20);}}
JunitPerfRule 就是我们前面提及的自定义规则。
JunitPerfRule
实现如下:
public class JunitPerfRule implements TestRule {
//region private fields// 省略内部变量//endregion@Overridepublic Statement apply(Statement statement, Description description) {
Statement activeStatement = statement;JunitPerfConfig junitPerfConfig = description.getAnnotation(JunitPerfConfig.class);JunitPerfRequire junitPerfRequire = description.getAnnotation(JunitPerfRequire.class);if (ObjectUtil.isNotNull(junitPerfConfig)) {
// Group test contexts by test classACTIVE_CONTEXTS.putIfAbsent(description.getTestClass(), new HashSet<EvaluationContext>());EvaluationContext evaluationContext = new EvaluationContext(description.getMethodName(), DateUtil.getSimpleDateStr());evaluationContext.loadConfig(junitPerfConfig);evaluationContext.loadRequire(junitPerfRequire);ACTIVE_CONTEXTS.get(description.getTestClass()).add(evaluationContext);activeStatement = new PerformanceEvaluationStatement(evaluationContext,statement,statisticsCalculator,reporterSet,ACTIVE_CONTEXTS.get(description.getTestClass()),description.getTestClass());}return activeStatement;}}
主要流程就是执行方法的时候,首先获取方法上的 @JunitPerfConfig
和 @JunitPerfRequire
注解信息,然后进行对应的执行统计。
Statement
Statement 是 junit4 中执行最核心的一个对象。
可以发现,这里根据注解信息,对这个实现重写为 PerformanceEvaluationStatement。
PerformanceEvaluationStatement 的核心实现如下:
/*** 性能测试 statement* @author 老马啸西风* @see com.github.houbb.junitperf.core.rule.JunitPerfRule 用于此规则*/
public class PerformanceEvaluationStatement extends Statement {
// 省略内部变量@Overridepublic void evaluate() throws Throwable {
List<PerformanceEvaluationTask> taskList = new LinkedList<>();try {
EvaluationConfig evaluationConfig = evaluationContext.getEvaluationConfig();// 根据注解配置,创建对应的执行线程数for(int i = 0; i < evaluationConfig.getConfigThreads(); i++) {
// 初始化执行任务PerformanceEvaluationTask task = new PerformanceEvaluationTask(evaluationConfig.getConfigWarmUp(),statement, statisticsCalculator);Thread t = FACTORY.newThread(task);taskList.add(task);// 子线程执行任务t.start();}//主线程沉睡等待Thread.sleep(evaluationConfig.getConfigDuration());} finally {
//具体详情,当执行打断时,被打断的任务可能已经开始执行(尚未执行完),会出现主线程往下走,被打断的线程也在继续走的情况for(PerformanceEvaluationTask task : taskList) {
task.setContinue(false); //终止执行的任务}}// 更新统计信息evaluationContext.setStatisticsCalculator(statisticsCalculator);evaluationContext.runValidation();generateReportor();}/*** 报告生成*/private synchronized void generateReportor() {
for(Reporter reporter : reporterSet) {
reporter.report(testClass, evaluationContextSet);}}}
这里是最核心的实现部分,主流程如下:
(1)根据配置,创建对应的任务子线程
(2)根据配置,初始化子任务,并且执行
(3)主线程进行沉睡等待
(4)主线程沉睡结束,打断子线程自行,更新统计信息
(5)根据统计信息,生成对应的测试报告文件
PerformanceEvaluationTask
子任务的实现也值得注意,核心实现如下:
public class PerformanceEvaluationTask implements Runnable {
/*** 热身时间*/private long warmUpNs;/*** junit statement*/private final Statement statement;/*** 统计计算者*/private StatisticsCalculator statisticsCalculator;/*** 是否继续标志位*/private volatile boolean isContinue;public PerformanceEvaluationTask(long warmUpNs, Statement statement, StatisticsCalculator statisticsCalculator) {
this.warmUpNs = warmUpNs;this.statement = statement;this.statisticsCalculator = statisticsCalculator;this.isContinue = true; //默认创建时继续执行}@Overridepublic void run() {
long startTimeNs = System.nanoTime();long startMeasurements = startTimeNs + warmUpNs;while (isContinue) {
evaluateStatement(startMeasurements);}}/*** 执行校验* @param startMeasurements 开始时间*/private void evaluateStatement(long startMeasurements) {
//0. 如果继续执行为 false,退出执行。if(!isContinue) {
return;}//1. 准备阶段if (nanoTime() < startMeasurements) {
try {
statement.evaluate();} catch (Throwable throwable) {
// IGNORE}} else {
long startTimeNs = nanoTime();try {
statement.evaluate();statisticsCalculator.addLatencyMeasurement(getCostTimeNs(startTimeNs));statisticsCalculator.incrementEvaluationCount();} catch (InterruptedException e) {
// NOSONAR// IGNORE - no metrics} catch (Throwable throwable) {
statisticsCalculator.incrementEvaluationCount();statisticsCalculator.incrementErrorCount();statisticsCalculator.addLatencyMeasurement(getCostTimeNs(startTimeNs));}}}/*** 获取消耗的时间(单位:毫秒)* @param startTimeNs 开始时间* @return 消耗的时间*/private long getCostTimeNs(long startTimeNs) {
long currentTimeNs = System.nanoTime();return currentTimeNs - startTimeNs;}//region getter & setterpublic boolean isContinue() {
return isContinue;}public void setContinue(boolean aContinue) {
isContinue = aContinue;}//endregion
}
这个任务,主要负责统计任务的耗时。
统计对应的成功数量、异常数量等。
通过 volatile 定义的 isContinue 变量,便于在主线程沉睡结束后,终止循环。
ps: 这里还是可以发现一个问题,如果 statement.evaluate();
已经开始执行了,那么无法被中断。这是一个可以改进的地方。
小结
本篇从 junit rules 讲起,分析了整个性能测试工具的实现原理。
总的来说,实现思路并不是很难,所有复杂的应用,都是有简单的部分组成。
文中为了便于大家理解,对源码部分做了大量简化。
如果想获取完整的源码,请前往开源地址:https://github.com/houbb/junitperf。
我是老马,期待与你的下次重逢。
当然,也许你可以发现这种方式还是不够优雅,junit5 为我们提供了更加强大的功能,我们下一节将讲解 junit5 的实现方式。
参考资料
https://github.com/houbb/junitperf
https://github.com/junit-team/junit4/wiki/Rules