1、 背景
长期以来,软件测试工程师都在如何提高软件系统质量和如何提高测试效率的道路上艰难地探索,但始终没有一款性能全面的测试工具可以满足需求。
ThreadingTest(简称“TT”) 智能型测试工具系列一期,是基于程序源代码的Android应用测试工具。采取前端分析器和后端结果分析分离的技术路线,实现对多种语言的编译器级分析和多维度测试。
ThreadingTest通过一系列自动、高效、可视化技术,使软件维护与开发效率加倍、成本减半、系统软件质量提高几个数量级。
ThreadingTest采用离线分析操作,即使电脑脱离互联网也可以获得同样的体验,保障代码的安全。
2、 ThreadingTest Android JUnit Test单元测试
ThreadingTest Android JUnit Test是基于Android JUnit Test,利用ThreadingTest本身的优势,编写的一套单元测试组件。在原有的Android JUnit Test测试驱动代码的基础上,只需要进行很小的改动,就可以修改为符合ThreadingTest Android JUnit Test测试代码的单元测试。除此之外,两者的测试几乎没有差别,只需要您在测试时,打开TT实时监控来接收数据,而在测试过程中,则无需其他操作,甚至感觉不到数据的收集过程。
对于传统的单元测试,在单元测试结束后,测试人员只能获得测试结果是否符合预期结果。如果与预期结果存在差异,也无法立即得到错误原因。
当使用ThreadingTest Android JUnit Test进行单元测试时,还是相同的测试体验,却能够得到白盒测试的数据,当单元执行结束后,可以根据产生的数据来分析程序的出错点,使Bug的查找更快速,更准确,而为此付出最小的代价。
3、 Android单元测试的编写
Android单元测试有两种方法,首先作为java程序,可以试用JUnit Test进行测试,另外也可使用Android JUnit Test进行单元测试。
1)、JUnit Test进行单元测试
JUnit对Android应用程序进行单元测试需要使用Java命令来启动或者在eclipse里面将启动的Bootstrap Entries改为JRE,但是这种只能测试逻辑代码,因为是是运行在JVM上,而不是Android系统中,所以不能测试Android有关的代码。
使用JUnit测试的代码,需要在函数的之前加@Test,函数必须为public类型,在eclipse中,在类上右击,选择JUnit Test即可进行测试。
2)Android JUnit Test进行单元测试
Android JUnit Test单元测试是一组直接或间接继承自junit.framework.Testcase的类集合,入口是InstrumentationTestRunner。
使用Android JUnit Test测试,也存在两种测试方法,测试驱动与测试代码在同一工程下和测试驱动和测试代码位于不同工程,两者之间并无巨大差异,只是在测试时,前者只安装一个App,后者安装两个App。
如下为一个单元测试的实例:
步骤 1:准备好需要测试的源代码
步骤2:添加测试类,编写测试代码
package calculator.xwg.test;
import java.util.ArrayList;
import java.util.LinkedList;
import android.test.ActivityInstrumentationTestCase2;
import android.app.AlertDialog;
import android.app.Instrumentation;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import calculator.xwg.CalculatorMainActivity;
public class CalculatorMainActivityTest extends
ActivityInstrumentationTestCase2<CalculatorMainActivity> {
private Instrumentation mInstrumentation;
private CalculatorMainActivity mActivity;
private TextView mQuestionText;
private TextView mAnswerText;
private Button mCalculateButton;
private Button mButton1;
private Button mButtonPlus;
private Button mButtonAngle;
private Button mButtonDegree;
private Button mButtonF6;
class AssertPair{
public AssertPair(CharSequence quest, CharSequence res){
question = quest;
result = res;
}
boolean assertResult(){
return (question.toString().compareTo(result.toString()) == 0);
}
public CharSequence question;
public CharSequence result;
}
ArrayList<AssertPair> assertList = new ArrayList<AssertPair>();
AssertPair mAssertPair;
FailureInfo mFailureInfo;
@SuppressWarnings("deprecation")
public CalculatorMainActivityTest() {
super("calculator.xwg", CalculatorMainActivity.class);
// TODO Auto-generated constructor stub
}
@Override
protected void setUp() throws Exception {
super.setUp();
mInstrumentation = getInstrumentation();
setActivityInitialTouchMode(false);
mActivity = (CalculatorMainActivity) getActivity();
mQuestionText = (TextView)mActivity.findViewById(calculator.xwg.R.id.textQuestion);
mAnswerText = (TextView)mActivity.findViewById(calculator.xwg.R.id.textAnswer);
mCalculateButton = (Button)mActivity.findViewById(calculator.xwg.R.id.button74);
mButton1 = (Button)mActivity.findViewById(calculator.xwg.R.id.button60);
mButtonPlus = (Button)mActivity.findViewById(calculator.xwg.R.id.button63);
mButtonAngle = (Button)mActivity.findViewById(calculator.xwg.R.id.button03);
mButtonDegree = (Button)mActivity.findViewById(calculator.xwg.R.id.button02);
mButtonF6 = (Button)mActivity.findViewById(calculator.xwg.R.id.buttonF6);
mActivity.engine.clearCustomFunctions();
} // end of setUp() method definition
@Override
protected void tearDown() throws Exception {
mActivity.engine.clearCustomFunctions();
super.tearDown();
}
public void testPreConditions() {
//assertTrue(mSpinner.getOnItemSelectedListener() != null);
//assertTrue(mPlanetData != null);
//assertEquals(mPlanetData.getCount(),ADAPTER_COUNT);
} // end of testPreConditions() method definition
public void testCalculate() {
LinkedList<AssertPair> checkItemList = new LinkedList<AssertPair>();
checkItemList.add(new AssertPair("1-2×((3+4)/58+8)×25%", "-3.06034482759"));
checkItemList.add(new AssertPair("root(16,4)", "2"));
checkItemList.add(new AssertPair("sin(0°)", "0"));
checkItemList.add(new AssertPair("sin(30°)", "0.5"));
checkItemList.add(new AssertPair("sin(390°)", "0.5"));
checkItemList.add(new AssertPair("sin(-330°)", "0.5"));
checkItemList.add(new AssertPair("sin(90°)", "1"));
checkItemList.add(new AssertPair("sin(145°)", "0.573576436351"));
checkItemList.add(new AssertPair("sin(180°)", "0"));
checkItemList.add(new AssertPair("sin(200°)", "-0.342020143326"));
checkItemList.add(new AssertPair("sin(270°)", "-1"));
checkItemList.add(new AssertPair("sin(300°)", "-0.866025403784"));
checkItemList.add(new AssertPair("sin(360°)", "0"));
checkItemList.add(new AssertPair("sin(π/6)", "0.5"));
checkItemList.add(new AssertPair("tan(0)", "0"));
checkItemList.add(new AssertPair("tan(0.2)", "0.202710035509"));
checkItemList.add(new AssertPair("tan(0.4)", "0.422793218738"));
checkItemList.add(new AssertPair("tan(0.6)", "0.684136808342"));
checkItemList.add(new AssertPair("tan(0.8)", "1.02963855705"));
checkItemList.add(new AssertPair("tan(1.0)", "1.55740772465"));
checkItemList.add(new AssertPair("tan(1.2)", "2.57215162213"));
checkItemList.add(new AssertPair("tan(1.4)", "5.79788371548"));
checkItemList.add(new AssertPair("tan(1.8)", "-4.28626167463"));
checkItemList.add(new AssertPair("tan(2.0)", "-2.18503986326"));
checkItemList.add(new AssertPair("tan(2.2)", "-1.37382305677"));
checkItemList.add(new AssertPair("tan(2.4)", "-0.916014289673"));
checkItemList.add(new AssertPair("tan(2.6)", "-0.60159661309"));
checkItemList.add(new AssertPair("tan(2.8)", "-0.355529831651"));
checkItemList.add(new AssertPair("tan(3.0)", "-0.142546543074"));
checkItemList.add(new AssertPair("tan(3.2)", "0.0584738544596"));
checkItemList.add(new AssertPair("tan(π/2)", "tan:Invalid input."));
checkItemList.add(new AssertPair("tan(π)", "0"));
checkItemList.add(new AssertPair("tan(π/2×3)", "tan:Invalid input."));
checkItemList.add(new AssertPair("tan(2×π)", "0"));
checkItemList.add(new AssertPair("tan(45°)", "1"));
checkItemList.add(new AssertPair("tan(90°)", "tan:Invalid input."));
checkItemList.add(new AssertPair("tan(405°)", "1"));
checkItemList.add(new AssertPair("tan(-315°)", "1"));
checkItemList.add(new AssertPair("tan(-675°)", "1"));
checkItemList.add(new AssertPair("1+1", "2"));
checkItemList.add(new AssertPair("(1+2i)×(3-4i)/(5+6i)", "1.0983607-0.91803279i"));
for(final AssertPair pair : checkItemList){
mActivity.runOnUiThread(
new Runnable() {
public void run() {
mQuestionText.setText(pair.question);
mActivity.onButtonClick(mCalculateButton);
}
}
);
mInstrumentation.waitForIdleSync();
assertEquals("The result of " + pair.question + ":", pair.result, mAnswerText.getText());
}
}
public void testConvert() {
mActivity.runOnUiThread(
new Runnable() {
public void run() {
mQuestionText.setText("1-i");
mActivity.onButtonClick(mButtonAngle);
}
}
);
mInstrumentation.waitForIdleSync();
assertEquals("1.4142136∠5.4977871", mAnswerText.getText());
mActivity.runOnUiThread(
new Runnable() {
public void run() {
mQuestionText.setText("1-i");
mActivity.onButtonClick(mButtonDegree);
}
}
);
mInstrumentation.waitForIdleSync();
assertEquals("1.4142136∠315°", mAnswerText.getText());
}
public void testContinue() {
mActivity.runOnUiThread(
new Runnable() {
public void run() {
mQuestionText.setText("1-i");
mActivity.onButtonClick(mButtonAngle);
assertList.add(new AssertPair("1.4142136∠5.4977871", mAnswerText.getText()));
mActivity.onButtonClick(mButtonDegree);
assertList.add(new AssertPair("1.4142136∠315°", mAnswerText.getText()));
}
}
);
mInstrumentation.waitForIdleSync();
for(AssertPair pair : assertList){
assertEquals(pair.question, pair.result);
}
assertList.clear();
}
public void testCustomFunction() {
mActivity.runOnUiThread(
new Runnable() {
public void run() {
mActivity.onButtonClick(mButtonF6);
mActivity.onButtonClick("5");
mActivity.onButtonClick(")");
mActivity.onButtonClick("=");
mFailureInfo = FailureInfo.checkFailure("Unknown keyword:F", mAnswerText.getText());
if(mFailureInfo != null) return;
mQuestionText.setText("π×#1×#1");
mActivity.onButtonClick("FS");
TextView tv = (TextView)mActivity.mDialog.findViewById(calculator.xwg.R.id.inputText);
tv.setText("Test");
Button ok = (Button)mActivity.mDialog.findViewById(calculator.xwg.R.id.buttonOK);
ok.performClick();
ListView lv = (ListView)((AlertDialog)mActivity.mDialog).getListView();
lv.requestFocus();
lv.setSelection(5);
lv.performItemClick(null, 5, 5);
mActivity.onButtonClick(mButtonF6);
mActivity.onButtonClick("5");
mActivity.onButtonClick(")");
mActivity.onButtonClick("=");
mFailureInfo = FailureInfo.checkFailure("78.5398163397", mAnswerText.getText());
if(mFailureInfo != null) return;
mActivity.engine.clearCustomFunctions();
}
}
);
mInstrumentation.waitForIdleSync();
if(mFailureInfo != null){
assertEquals(mFailureInfo.mExpect, mFailureInfo.mResult);
mFailureInfo = null;
}
}
}
步骤3:修改AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="calculator.xwg"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="8" />
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<application android:label="@string/app_name" android:icon="@drawable/calculator">
<uses-library android:name="android.test.runner" />
<activity android:name=".CalculatorMainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="HelpActivity"></activity>
</application>
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="calculator.xwg"
android:label="Tests for TT calculator." />
</manifest>
注意:绿色背景为添加部分。其中android:label="Tests for TT calculator."在模拟器中的Dev Tools工具中,设置Instrumentation下显示的名称,在步骤4运行中,点击该标签,也可运行单元测试。在ThreadingTest Android JUnit Test中也是重要一步,可以设置多个单元,多个名称,每个名称尽量不要重复,以区分不同的单元测试。
步骤4:运行测试类
运行测试类有三种方式,包括:
1、 命令行方式
使用adb命令启动单元测试
2、 eclipse中选择Android JUnit Test运行方式
在eclipse中,右击测试工程,选择run as -> Android JUnit Test
3、 模拟器中,使用Dev Tools
在模拟器中安装该程序后,在Dev Tools工具中,选择Instrumentation下显示的与android:label同名的标签运行单元测试。
4、 ThreadingTest单元测试的编写
要使用ThreadingTest进行单元测试类的编写,需要将所有的测试类写到一个单独的文件夹下,在进行编译工程时,使用参数-filter将其从编译路径中排除,不对其进行插桩。如下图所示:
被测源码放在src文件夹中,单元测试驱动代码放在test_src文件下。
其他方面,需要在Android单元测试上进行的修改为:
1、引入ThreadingTest Android jar包,JavaParser-Android.jar和通信包jeromq-0.3.0-SNAPSHOT.jar。
2、将继承Android JUnit Test类换成继承ThreadingTest Android JUnit Test类,ThreadingTest Android JUnit Test类中与Android JUnit Test相对应的类为在类名前加TT。例如与InstrumentationTestCase相对应的类为TTInstrumentationTestCase,TT Android JUnit Test类所在的包为com.zoa.android.test。
3、在重载了setUp和tearDown函数的测试类中,需要分别调用super.setUp()和super.tearDown()函数,否则无法对数据进行惊醒测试用例的区分,如果没无需进行初始化和资源释放则不用继承setUp()和tearDown()函数。
4、其他操作同Android JUnit Test操作相同。
如下为一个与上面Android JUnit Test单元测试类相对应的TT Android JUnit Test单元测试类,添加背景色的为需要注意的点,其他的与原测试代码相同,无需改动:
package calculator.xwg.test;
import java.util.ArrayList;
import java.util.LinkedList;
import com.zoa.android.test.TTActivityInstrumentationTestCase2;
import android.app.AlertDialog;
import android.app.Instrumentation;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import calculator.xwg.CalculatorMainActivity;
public class CalculatorMainActivityTest extends
TTActivityInstrumentationTestCase2<CalculatorMainActivity> {
…// 与上面相同的代码和描述
@Override
protected void setUp() throws Exception {
super.setUp(); // 如重写setUp函数,此句为必须
…// 与上面相同的代码和描述
} // end of setUp() method definition
@Override
protected void tearDown() throws Exception {
…// 与上面相同的代码和描述
super.tearDown(); //如重写tearDown函数,此句为必须
}
…// 与上面相同的代码和描述
}
5、 ThreadingTest单元测试的编译
编写完成TT Android JUnit Test 测试类之后,使用TT对工程进行编译。
1、修改编译文件,如果是单一编码格式的工程,需要修改%TT_Path%/ant-build-a/android-instru_en.xml文件,多种编码格式源码的工程,则需要修改%TT_Path%/ant-build-a/android-instru_code_en.xml文件。修改内容为,添加-filter参数,修改方法为:
添加filterpath属性,用于过滤不需要插桩的单元测试驱动类文件。
<property location="不需要插桩编译的路径,这里为test_src路径" name="filterpath"/>
…
<arg line=""${propath}" "${TT}" -tt="${testpropath}" -encoding=${encoding} -filter="${filterpath}" -s="${testpropath}/src""/>
在运行JavaParser处,添加绿色背景参数。
注:demo版本中需手动修改,后续版本将对此进行改进。
2、打开TT,创建一个新的工程,与非单元测试工程的编译相同。详见: http://www.threadingtest.com/xwiki/bin/view/ZOA%7C4.演示/Android+程序测试配置
6、 ThreadingTest单元测试的运行
在真机中测试时,需要安装Dev Tools工具。模拟器中附带该工具。
将上一步中,TT编译生成的APK文件,安装到模拟器或真机中,安装之后,会在Dev Tools工具下,Instrumentation中会出现与AndroidManifest.xml文件中android:label设置的同名的标签,点击可运行ThreadingTest单元测试。
在运行ThreadingTest单元测试时,需要打开TT接收数据,否则测试会阻塞在消息发送处而无法进行。
模拟器测试步骤为:
1、打开TT,选择将要运行的测试的工程,然后打开实时监控界面,新建一个测试用例。
2、打开模拟器。
3、进行端口重定向
双击运行adb-android.bat,运行的命令为:
adb forward tcp:15555 tcp:15555
adb forward tcp:15556 tcp:15556
adb forward tcp:15557 tcp:15557
adb forward tcp:15558 tcp:15558
其中adb需要在环境变量中配置好,如果未配置,也可手动切换到%Android_sdk%\platform-tools目录下手动打开命令行窗口,运行上述命令。
如果运行adb forward –list出现:
其中,emulator-5554也可能是其他名称,则说明配置成功。
4、勾选上测试用例,set Ip为127.0.0.1,确定后点击开始。
5、打开Dev Tools,点击Instrumentation进入,点击工程中设置的标签名,如Tests for TT calculator.
点击后,开始运行单元测试。
此时TT实时监控界面会有数据的接收:
单元测试运行运行结束后,右击测试用例树右上角的刷新按钮,选择Update Testcase
会增加红色框中的测试用例,首先是与勾选的测试用例同级同名的测试类型,然后依次为测试类所在的包名,类名以及运行测试函数名。
在单元测试开始之前,被测程序的初始化数据会被保存在初始勾选的测试用例中,这里为UnitTest中。单元测试开始之后,产生的测试数据会被保存到,与测试驱动函数对应的测试用例当中。
TT单元测试完成。
真机测试步骤为:
1、打开TT,选择将要运行的测试的工程,然后打开实时监控界面,新建一个测试用例。
2、勾选上测试用例,set Ip为真机的Ip地址,确定后点击开始。
3、同模拟器测试步骤5。