7.3 Java算法测试案例
本节将结合一个具体案例来讲解如何借助Java Vuser来测试Java程序的算法。在案例中,主要模拟了测试某银行的信用卡审批过程,这部分内容是开发阶段性能测试的一部分。在这个测试例子中,主要发现了在并发时的两个算法问题:提交任务处理结果发生异常时Socket没有正常关闭;申请任务方法giveOutWork()没有加同步控制关键字synchronized。
为了更好地演示测试效果,程序中忽略了实际程序中的一些细节,例如具体的任务申请以及处理过程。
测试内容简介
信用卡审批程序主要包括两个部分,即客户端程序与服务器端程序。客户端程序包含一个Client.java类文件,即仅包含一个类Client,主要封装客户端的“申请—处理—提交”操作。服务器端程序即WorkServer.java,包含WorkQueue、AcceptClientThread、WorkServer三类。类WorkQueue主要完成任务队列的构建与管理工作;类AcceptClientThread继承线程类Thead,以独立线程的方式来处理客户端申请任务并保存客户端对任务的处理结果;类WorkServer是服务器端的执行类,主要完成对WorkQueue、AcceptClientThread的调用。
下面具体介绍业务流程。客户端对一项任务的业务流程如下:
第一步:与服务器建立连接,向服务器发出处理任务申请,等待服务器返回任务;
第二步:从服务器得到任务后,开始进行处理;
第三步:处理完毕后,提交结果给服务器进行保存,然后等待服务器返回结果;
第四步:输出服务器的保存结果;
第五步:结束当前的任务处理。
客户端源程序清单:Clien.java
package com.loadrunner.test;
import java.io.*;
import java.net.*;
/**
* 客户端{申请任务、确认是否可以审批、处理、传递结果得到确认}
* @author ChenShaoying
*/
public class Client {
Socket socket;
int clientNumber;
BufferedReader is;//读出服务器返回的输入流
PrintWriter os;//反馈给服务器的输出流
/**
* 向服务器申请任务
*/
Client(Socket s) {
try {
this.socket = s;
this.is = new BufferedReader(new InputStreamReader(s
.getInputStream()));
this.os = new PrintWriter(s.getOutputStream());
this.clientNumber = Integer.parseInt(is.readLine());
} catch (Exception e) {
System.err.println("Error:Can not init the network!");
}
}
public int applyWork() {
int workNumber=-1;
try {
this.os.println("Apply");//发出申请
os.flush();
workNumber = Integer.parseInt(this.is.readLine());//读出申请结果
if (workNumber == -1) {
System.out.println("Server has no Work to do");
System.exit(1);//退出程序
}
} catch (Exception e) {
System.err.println("Error:Can not apply the network!");
}
return workNumber;
}
/**
* 处理任务:添加实际处理过程即可,本处略
* @return deal with result
* @author ChenShaoying
*/
public int dealWithWork(int worknumber) {
System.out.println("dealWithWork:"+worknumber);
return 1;
}
/**
* 传递结果到服务器确认
* @return ensure result
* @author ChenShaoying
*/
public boolean finishWork(int workNumber) {
boolean finish=false;
try {
this.os.println("finish");
os.flush();
finish = Boolean.valueOf(this.is.readLine()).booleanValue();
if (finish == false) {
System.out.println("Error:Work finish can not be set!");
System.exit(1);
}
} catch (Exception e) {
System.err.println("Error:Can not start the network!");
System.exit(1);
}
return finish;
}
}
服务器端对一项任务的业务流程如下:
第一步:建立任务队列,等待审批人员进行申请;
第二步:服务器收到用户申请后,系统会先锁定记录;
第三步:修改当前记录状态,并把当前任务返回给客户端;
第四步:等待客户端审批人员返回处理结果;
第五步:收到客户端提交的处理结果后,保存处理结果。
服务器端源代码清单:WorkServer.java
package com.loadrunner.test;
import java.io.*;
import java.net.*;
/**
* 队列{原始N个任务,接受申请返回任务号,检查任务是否正在处理、接受审批任务确认}
* @author ChenShaoying
*/
class WorkQueue{
private int []WorkFlag;//0-未申请;1-申请后正在处理;2-处理完成
private int total;
int nowNumber;
//创建任务队列:total-队列长度;WorkFlag-用来监控队列中每个任务状态的数组;nowNumber-当前可以申请到的任务编号
WorkQueue(int totalNumber)
{
this.total=totalNumber;
this.WorkFlag=new int [this.total];
for(int i=0;i<this.total;i++)
{
this.WorkFlag[i]=0;
}
this.nowNumber=1;
}
//接受客户端申请,把队列任务提供给当前申请的客户端
int giveOutWork()
{
int k=this.nowNumber;
this.WorkFlag[this.nowNumber]=1;
try {
Thread.sleep(1);//模拟服务器对任务的处理时间
} catch (InterruptedException e) {
e.printStackTrace();
}
this.nowNumber++;
return k;
}
//如果当前任务的状态是正在处理,则修改其状态为完成并返回true,否则返回false。
boolean finishWork(int worknumber)
{
int number=worknumber;
if (this.WorkFlag[number]==1)
{
this.WorkFlag[number]=2;
return true;
}else{
System.err.println("Work "+number+" Can not be finished");
}
return false;
}
}
/**
* 客户端连接对话线程{接受任务申请返回任务号、接受审批任务确认、接受任务处理结果、返回确认消息}
*
*/
class AcceptClientThread extends Thread
{
private Socket socket=null;
private int clientNumber;
private WorkQueue workQueue;
AcceptClientThread(Socket socket,WorkQueue q,int clientNumber)
{
this.socket=socket;
this.workQueue=q;//初始化对任务队列的管理
this.clientNumber=clientNumber;
}
int giveOutWork()//分配任务
{
try{
sleep(100);//延迟100毫秒分派,用于模拟实际工作中分发前的准备工作
}catch(Exception e)
{
System.err.println(e);
System.exit(0);
}
return workQueue.giveOutWork();
}
boolean finishWork(int worknumber) //结束工作
{
return workQueue.finishWork(worknumber);
}
public void run()
{
try{
//创建输入输出流
BufferedReader is=new
BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter os=new PrintWriter(socket.getOutputStream());
os.println(this.clientNumber);
os.flush();
//1.接受任务申请返回任务号
String step=is.readLine();
while(step.equals("Apply")==false)
{
sleep((int)Math.random()*100);
step=is.readLine();
}
int worknumber=this.giveOutWork();
os.println(worknumber);//任务号返回给客户端
os.flush();
//2.任务处理完毕后,把处理结果返回给服务器
step=is.readLine();
while(step.equals("finish")==false)
{
sleep(100);
step=is.readLine();
}
//3.返回确认消息,开始提交客户端的处理结果,
//如果没有被处理过(状态为1),则可以提交客户端的结果
boolean result=this.finishWork(worknumber);
os.println(result);
os.flush();
if(result==true)
{
System.out.println("Work "+Integer.toString(worknumber)
+"done by client "+Integer.toString(this.clientNumber)+
".");
}
//关闭连接和输入输出流
os.close();
is.close();
socket.close();
}
catch(Exception e)
{
System.err.println(e);
}
}
}
public class WorkServer {
public static void main(String[] args) {
// TODO Auto-generated method stub
ServerSocket serverSocket = null;
boolean listening = true;
WorkQueue queue=new WorkQueue(200000);
//创建一个端口监听
try {
serverSocket = new ServerSocket(8000);
}
catch (IOException e)
{
System.err.println("Could not listen on port: 8000.");
System.exit(-1);
}
try
{
int clientnumber=0;
while (listening)
{
Socket socket=new Socket();
socket = serverSocket.accept(); //程序将在此等候客户端的连接
clientnumber++;
//客户申请后将启动一个独立线程来处理客户申请
new AcceptClientThread(socket,queue,clientnumber).start();
}
serverSocket.close();
}
catch(Exception e)
{
System.err.println(e);
//System.exit(-1);
}
}
}
测试源程序
测试思路很简单,主要是模拟多个客户端并发申请与处理任务,因此采用了手工Java虚拟用户。为了方便程序开发,测试程序Test.java先在Eclipse中开发完成。在Test.java类文件中,编写具体的测试执行类Test,用于调用Client.java中的方法。
下面是测试程序Test.java的程序清单:
测试程序清单:Test.java
package com.loadrunner.test;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
public class Test {
public void ApplyProccess() throws IOException
{
Socket clientSocket = null;
try {
//建立服务器连接,创建输入输出流
clientSocket = new Socket("127.0.0.1",8000);
Client client=new Client(clientSocket);
//1申请任务号
int worknumber=client.applyWork();
//2处理记录
int result=client.dealWithWork(worknumber);
//3发送处理结果到服务器确认
boolean ensureResult=client.finishWork(worknumber);
if(ensureResult!=true)
{
System.err.println("Error:Work check error!");
System.exit(0);
}
else
{
System.out.println("Finish work No."
+Integer.toString(worknumber));
}
} catch (UnknownHostException e) {
System.err.println("Don't know about host: 127.0.0.1.");
System.exit(1);
} catch (IOException e) {
System.err.println("Couldn't get I/O for the connection to:
127.0.0.1."+e);
System.exit(1);
}
//关闭服务器连接
clientSocket.close();
}
}
虚拟用户脚本
上面三个程序在Elipse中编译完成后,将会按照类文件的包名称“com.loadrunner.test”生成对应的目录结构“com\loadrunner\test”,下面可以看到编译后的class文件。
启动VuGen,先创建空的虚拟用户脚本“SimpleJava”,然后把程序的编译结果放到虚拟用户脚本目录下,如图7-20所示。

图7-20 虚拟用户脚本结构
上面的工作完成后,接下来需要修改脚本,以调用Test类中的Test()方法。修改后的脚本如图7-21所示。
在Eclipse中运行WorkServer.java,启动WorkServer服务器后才可以调试脚本。在VuGen中运行脚本,如果在运行结果Log中看到“Finish work No.*”,则表示脚本运行正确,可以成功申请并处理任务。图7-22所示为成功申请并处理了1号任务。

图7-21 修改后的脚本

图7-22 成功处理任务后的运行结果
创建与执行场景
虚拟用户脚本通过调试后,接下来要放到Controller中创建场景。首先运行一个用户,以在Controller中验证脚本的正确性。把脚本迭代次数设置为200,部分运行结果如图7-23所示,说明脚本在Controller中运行正常。
把并发用户变为10个,运行场景,并发申请任务开始发生错误:图7-24是场景运行状态;图7-25是WorkServer运行结果。从服务器上的提示可以看出,Socket连接发生错误。如果没有正常关闭,则会有“
”异常。

图7-23 单用户成功处理任务后的运行结果

图7-24 10个用户并发时的场景状态

图7-25 用户并发时的WorkServer状态
分析这个错误的具体原因很容易,Socket连接发生重置多是由于非正常关闭Socket所致。浏览一下Test.java可以看到程序中有很多System.exit()语句,这种语句会导致直接退出程序而没有执行最后的语句clientSocket.close()。当任务处理过程发生异常时,无疑会导致Socket连接没有正常关闭。解决的方法很简单,在System.exit()语句前加上clientSocket.close()即可。
修正Socket连接缺陷后,10个用户并发时的WorkServer运行信息如图7-26所示,可以看到服务器不能正常提交处理结果。

图7-26 成功处理任务后的运行结果
为了详细追踪问题,需要更改测试程序以及服务器程序。Java虚拟用户脚本需要输出一些信息到控制台,而WorkServer则需要输出不能提交保存结果的任务状态。
新的虚拟用户Actions部分的程序清单如下:
package com.loadrunner.test;
import lrapi.lr;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
public class Actions
{
public int init() {
return 0;
}//end of init
public int action() {
try {
lr.rendezvous("申请任务");
this.ApplyProccess();
}
catch (java.io.IOException e) {
e.printStackTrace();
}
return 0;
}//end of action
public int end() {
return 0;
}//end of end
public void ApplyProccess() throws IOException
{
Socket clientSocket = null;
try { //建立服务器连接,创建输入输出流
clientSocket = new Socket("127.0.0.1",8000);
Client client=new Client(clientSocket);
//1申请任务号
int worknumber=client.applyWork();
//2处理记录
int result=client.dealWithWork(worknumber);
//3发送处理结果到服务器确认
boolean ensureResult=client.finishWork(worknumber);
if(ensureResult!=true)
{
lr.error_message("Error:Work "+worknumber+"finish error!");
//System.err.println("Error:Work check error!");
//clientSocket.close();
// System.exit(1);
}
else
{
System.out.println("Finish work No."+Integer.toString
(worknumber));
}
} catch (UnknownHostException e) {
System.err.println("Don't know about host: 127.0.0.1.");
} catch (IOException e) {
System.err.println("Couldn't get I/O for the connection to: 127.0.0.1."+e);
}
//关闭服务器连接
clientSocket.close();
}
}
程序中用“lr.error_message("Error:Work "+worknumber+"finish error!");”语句替换了“System.err.println("Error:Work check error!");”,目的是向Controller的控制台发出消息。
WorkServer类中则修改了finishWork(int worknumber)方法,把其中的“System.err. println("Work"+number+"Can not be finished");”替换成“System.err.println("Work "+number+" Can not be finished,"+"WorkFlag is "+WorkFlag[number]);”,以查找不能保存处理结果的当前状态任务。修改后的程序如下:

再次选择10个用户并发, Controller将会弹出一些错误提示,如图7-27所示。

图7-27 Controller运行时捕获的一些错误
WorkServer服务器弹出的消息如图7-28所示,可以看出不能提交处理结果的任务的状态标志为2,表示已经由其他用户处理完毕,因此提交发生错误。
通过客户端以及服务器的错误信息,基本可以断定任务分配存在重复现象——只有把同一任务分给多个客户端进行处理,才会发生不能提交保存结果的状况。这时自然会想到giveOutWork()方法可能存在问题。检查giveOutWork()方法,发现根本没有做并发同步控制!

图7-28 WorkServer运行结果日志
修正后的giveOutWork()方法如下所示,加了同步关键字synchronized。
synchronized int giveOutWork()
{
int k=this.nowNumber;
this.WorkFlag[this.nowNumber]=1;
try {
Thread.sleep(1);//模拟服务器对任务的处理时间
} catch (InterruptedException e) {
e.printStackTrace();
}
this.nowNumber++;
return k;
}
再次运行并发场景,则可以看到任务处理过程完全正确,图7-29即为添加同步控制后的WorkServer运行日志。

图7-29 添加同步控制后的WorkServer运行日志
至此,已经完成了对算法测试以及缺陷修正工作。
本节案例中的程序缺陷看似很容易发现,但在实际项目中是在测试一段时间后才发现并发分配算法存在问题的。读者可以把giveOutWork()方法中模拟服务器对任务的处理时间即Thread.sleep(1)语句注释后再进行并发测试,这时几乎很难再现前面问题,尽管把同一任务分给多个用户进行处理的缺陷仍然存在。
调整后的giveOutWork()方法如下:
int giveOutWork()
{
int k=this.nowNumber;
this.WorkFlag[this.nowNumber]=1;
/* try {
Thread.sleep(1);//模拟服务器对任务的处理时间
} catch (InterruptedException e) {
e.printStackTrace();
}*/
this.nowNumber++;
return k; }
通过本案例可以看出,很多算法需要认真全面的测试才可以挖出隐藏很深的缺陷。







