习惯于JUnit做功能方面unit test,而对于有些Test需要有一定的压力来模拟一定并发的读和写,借助JMeter来实现这样的测试框架是很不错的一个选择,一来减少很多工作量 (只需少量的定制:比如实现自己的ThreadGroup来定制并发线程的创建和执行,实现自己的Sampler来定制测试目标类的实例化和运行),二来可以很方便使用Hudson进行持续集成, 这对于利用Hudson进行持续集成的项目是再方便不过了,每次build之后除了JUnit报告之外,还能看到jtl报告来监视的性能状况。如果要这样做,首先要研究一下JMeter代码。
咋看JMeter框架,貌似挺复杂的,其实不然,基于上面的需求,我们只需要Java Sampler的测试,所以大部分的Jar包就可以排除,只需区区10个JAR包:
commons-logging-1.1.1.jar
xstream-1.4.2.jar
commons-lang3-3.1.jar
ApacheJMeter_java.jar
xmlpull-1.1.3.1.jar
logkit-2.0.jar
commons-io-2.2.jar
avalon-framework-4.1.4.jar
ApacheJMeter_core.jar
jorphan.jar
好吧,要研究10个JAR包的代码也不算少!幸运的是,剔除commons/xml/log相关的,真正需要研究的只有ApacheJMeter_core.jar/ApacheJMeter_java.jar,最多再了解下jorphan.jar。
首先,从JMeter的headless运行入手:$ jmeter -n -p user.properties -t my_test_plan.jmx -l my_results.jtl
其实就是调用 NewDriver.main("-n -p user.properties -t my_test_plan.jmx -l my_results.jtl").NewDriver类位于ApacheJMeter.jar中,之所以该jar没有列入上面10个之中是因为这个jar 包非常简单,只是一个headless运行的入口而已,真正项目要集成JMeter跟本不会去用这个类。作为CommandLine运行入 口,NewDriver只做两件事情,一是设置一些JMeter路径(主要是为了程序能找到 user.properties,saveservice.properties,和相关JAR的classpath),二是去反射运行 JMeter.start方法。
再来看位于ApacheJMeter_core.jar中JMeter类,抛开GUI和Remote test相关的代码,简单说,JMeter做的事情主要有
1. 解析命令行参数,加载user.properties(初始化log)
2. 查找并加载saveservice.properties
3. 将测试文件(.jmx文件)解析成HashTree
4. 创建一个StandardJMeterEngine,并把测试的工作交给JMeterEngine
当然,他还有其他重要的职责,比如监听所有的JMeterEngine,当接收到GUI的StopTestNow/Shutdown等命令时候来调用JMeterEngine相应的方法。
接下来研究下JMeterEngine,很简单的一个接口:
public interface JMeterEngine { void configure(HashTree testPlan); void runTest() throws JMeterEngineException; void stopTest(boolean now); void reset(); void setProperties(Properties p); void exit(); boolean isActive(); }
JMeterEngine只依赖HashTree, 只要能够构造一个HashTree, 就可以简单的调用其runTest方法完成所有的测试工作了. 而HashTree是由执行的测试文件如my_test_plan.jmx解析而来,其结构一般如下:
从xml tree看,对JMeter内部类应该有这样的直觉:
单纯从数据结构上可以这么理解,一个testplan包含多个ThreadGroup,每个ThreadGroup可以起一组线程跑相应JavaSampler测试,每个测试由相应的ResultCollector收集结果并生产report。其实并没有这么简单,这里有最重要的两个类可以进行扩展,实现和自己项目的集成,就是ThreadGroup和JavaSampler,这在本文开头就提过,至于如何扩展等分析完这些组件再写。本文接下来简单的以实例代码结束:
1. 构建一个Mockup HashTree,这样就能摆脱以命令行跑需要提供一个JMX测试文件的依赖:
public static HashTree createMockHashTree() throws IOException{ TestPlan tp = testPlan(); ThreadGroup group = threadGroup(); JavaSampler sampler = javaSampler(); ResultCollector resultCol = resultCollector(); HashTree tree = new HashTree(); tree.add(tp); HashTree groupTree = new HashTree(); groupTree.add(group); HashTree samplerTree = new HashTree(); samplerTree.add(sampler); HashTree resultTree = new HashTree(); resultTree.add(resultCol); samplerTree.add(samplerTree.getArray()[0], resultTree); groupTree.add(groupTree.getArray()[0],samplerTree); tree.add(tree.getArray()[0], groupTree); return tree; } public static TestPlan testPlan() throws IOException{ TestPlan tp = new TestPlan(); tp.setName("MyTest"); tp.setProperty(TestElement.TEST_CLASS, "TestPlan"); tp.setProperty(TestElement.GUI_CLASS,"TestPlanGui"); tp.setProperty(TestElement.ENABLED, true); tp.setComment(""); tp.setFunctionalMode(false); tp.setSerialized(false); return tp; } public static ThreadGroup threadGroup() throws IOException{ ThreadGroup tp = new ThreadGroup(); tp.setName("PerformanceGroup"); tp.setProperty(TestElement.TEST_CLASS, "ThreadGroup"); tp.setProperty(TestElement.GUI_CLASS,"ThreadGroupGui"); tp.setProperty(TestElement.ENABLED, true); tp.setProperty(AbstractThreadGroup.ON_SAMPLE_ERROR, "continue"); LoopController lc = new LoopController(); lc.setName("Loop Controller"); lc.setLoops(10); tp.setSamplerController(lc); tp.setNumThreads(5); tp.setRampUp(1); tp.setStartTime(System.currentTimeMillis()); tp.setEndTime(System.currentTimeMillis()); tp.setScheduler(false); return tp; } public static JavaSampler javaSampler() throws IOException{ JavaSampler tp = new JavaSampler(); tp.setName("PerformanceTest"); tp.setProperty(TestElement.TEST_CLASS, "JavaSampler"); tp.setProperty(TestElement.GUI_CLASS,"JavaTestSamplerGui"); tp.setProperty(TestElement.ENABLED, true); tp.setClassname("app.PerformanceTest"); return tp; } public static ResultCollector resultCollector() throws IOException{ ResultCollector tp = new ResultCollector(); tp.setName("Aggregate Report"); tp.setProperty(TestElement.TEST_CLASS, "ResultCollector"); tp.setProperty(TestElement.GUI_CLASS,"StatVisualizer"); tp.setProperty(TestElement.ENABLED, true); tp.setErrorLogging(false); tp.setFilename("/home/wilson.wu/unittest/report/performentce2.jtl"); return tp; }
2. 有了HashTree, 最简单的做法:
StandardJMeterEngine jmeterEngine = new StandardJMeterEngine(); jmeterEngine .configure(tree); jmeterEngine .runTest();
最后,要注意一点,在编程调用JMeter相关JAR之前,运行相关配置文件的设置,例如:
JMeterUtils.loadJMeterProperties(userProperties); JMeterUtils.initLogging(); JMeterUtils.initLocale(); JMeterUtils.setJMeterHome(""); // Set some (hopefully!) useful properties long now=System.currentTimeMillis(); JMeterUtils.setProperty("START.MS",Long.toString(now));// $NON-NLS-1$ Date today=new Date(now); // so it agrees with above JMeterUtils.setProperty("START.YMD",new SimpleDateFormat("yyyyMMdd").format(today));// $NON-NLS-1$ $NON-NLS-2$ JMeterUtils.setProperty("START.HMS",new SimpleDateFormat("HHmmss").format(today));// $NON-NLS-1$ $NON-NLS-2$