前言

上一节介绍了 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 {
@Override
public 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;
} @Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public 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 { @Rule
public final TestLogger logger = new TestLogger(); @Test
public 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 {

    @Rule
public 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 @Override
public 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 class
ACTIVE_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 { // 省略内部变量 @Override
public 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; //默认创建时继续执行
} @Override
public 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 & setter
public 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

关于 junit4 90% 的人都不知道的特性,详解 junitperf 的实现原理的更多相关文章

  1. 90%的人都不知道的Node.js 依赖关系管理(上)

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://dzone.com/articles/nodejs-dependency-mana ...

  2. 90%的人都不知道的Node.js 依赖关系管理(下)

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://dzone.com/articles/node-dependency-manage ...

  3. 关于setTimeout()你所不知道的地方,详解setTimeout()

    关于setTimeout()你所不知道的地方,详解setTimeout() 前言:看了这篇文章,1.注意setTimeout引用的是全部变量还是局部变量了,当直接调用外部函数方法时,实际上函数内部的变 ...

  4. 90%的开发者都不知道的UI本质原理和优化方式

    前言 很多开发者在工作中一直和UI打交道,所以认为UI非常的简单! 事实上对于90%的开发者来说,不知道UI的本质原理. 虽然在开发中,我们在接到产品的UI需求之后,可以走捷径照抄大型APP代码,但是 ...

  5. 99% 的人都不知道的 Kubernetes 网络疑难杂症排查方法

    原文链接:Kubernetes 网络疑难杂症排查分享 大家好,我是 roc,来自腾讯云容器服务 (TKE) 团队,经常帮助用户解决各种 K8S 的疑难杂症,积累了比较丰富的经验,本文分享几个比较复杂的 ...

  6. 很多人都不知道的监听微信、支付宝等移动app及浏览器的返回、后退、上一页按钮的事件方法

    版权声明:本文为博主原创文章,未经博主允许不得转载. 在实际的应用中,我们常常需要实现在移动app和浏览器中点击返回.后退.上一页等按钮实现自己的关闭页面.调整到指定页面或执行一些其它操作的 需求,那 ...

  7. 90% 前端开发者都不知道的 JavaScript 实用小技巧

    面试神器之数组去重 const a = [...new Set([1, 2, 3, 3])] >> [1, 2, 3] 操作数组担心 falsy 值? const res = myArra ...

  8. 大部分人都不知道的8个python神操作

    01 print 打印带有颜色的信息 大家知道 Python 中的信息打印函数 Print,一般我们会使用它打印一些东西,作为一个简单调试. 但是你知道么,这个 Print 打印出来的字体颜色是可以设 ...

  9. 震惊!90%的程序员不知道的Java知识!

    震惊!90%的程序员不知道的Java知识! 初学Java的时候都会接触的代码 public static void main(String[] args){ ... } 当时就像背公式一样把这行代码给 ...

随机推荐

  1. NVIDIA深度学习Tensor Core性能解析(上)

    NVIDIA深度学习Tensor Core性能解析(上) 本篇将通过多项测试来考验Volta架构,利用各种深度学习框架来了解Tensor Core的性能. 很多时候,深度学习这样的新领域会让人难以理解 ...

  2. CVPR2020:点云三维目标跟踪的点对盒网络(P2B)

    CVPR2020:点云三维目标跟踪的点对盒网络(P2B) P2B: Point-to-Box Network for 3D Object Tracking in Point Clouds 代码:htt ...

  3. python常识系列20-->python利用xlutils修改表格内容

    前言 世上的事,只要肯用心去学,没有一件是太晚的.要始终保持敬畏之心,对阳光,对美,对痛楚. 一.xlutils是什么? 是一个提供了许多操作修改excel文件方法的库: 属于python的第三方模块 ...

  4. 面试一次问一次,HashMap是该拿下了(一)

    文章目录 前言 一.HashMap类图 二.源码剖析 1. HashMap(jdk1.7版本) - 此篇详解 2. HashMap(jdk1.8版本) 3. ConcurrentHashMap ~~ ...

  5. NX二次开发-通过数组创建矩阵

    函数:UF_CSYS_create_matrix() 函数说明:通过数组创建矩阵. 用法: #include <uf.h> #include <uf_csys.h> exter ...

  6. 『动善时』JMeter基础 — 45、脚本录制工具Badboy介绍

    目录 1.Badboy软件介绍 2.Badboy下载 3.Badboy安装 4.Badboy界面介绍 (1)菜单栏: (2)工具栏: (3)左下角界面视图: 1.Badboy软件介绍 Badboy是一 ...

  7. 【源码分析】- 在SpringBoot中你会使用REST风格处理请求吗?

    ​ 目录 前言 1.什么是 REST 风格 1.1  资源(Resources) 1.2  表现层(Representation) 1.3  状态转化(State Transfer) 1.4  综述 ...

  8. CosId 1.0.3 发布,通用、灵活、高性能的分布式 ID 生成器

    CosId 通用.灵活.高性能的分布式 ID 生成器 介绍 CosId 旨在提供通用.灵活.高性能的分布式系统 ID 生成器. 目前提供了俩大类 ID 生成器:SnowflakeId (单机 TPS ...

  9. jenkins pipeline的声明式与脚本式

    自从Jenkins 2.0 版本升级之后,支持了通过代码(Groovy DSL)来描述一个构建流水线,灵活方便地实现持续交付,大大提升 Jenkins Job 维护的效率,实现从 CI 到 CD 到转 ...

  10. 解决CentOS下service 功能 不能使用 bash: service: command not found

    首先检查自己是否 使用的是root用户 在centos系统中,如果/sbin目录下没有service这个命令,就会出现 bash: service: command not found 解决步骤如下: ...