最近在一个项目中使用了junit 测试,总结一下。
Junit, 白盒的,自动化的,单元测试,回归测试。
Junit 最开始是用在java开发中的,android 也支持该测试框架。在android中我们使用junit一般有两种测试,一个是针对方法的单元测试,一个针对逻辑测试,而逻辑测试我们使用Robotium测试。
一、针对方法的单元测试
1、纯java的单元测试
这是一个只会做两数加减的超级简单的计算器(小学一年级必备极品)。代码如下:
publicclass SampleCalculator
{
public int add(int augend , int addend)
{
return augend + addend ;
}
public int subtration(int minuend , int subtrahend)
{
return minuend - subtrahend ;
}
}
将上面的代码编译通过。下面就是我为上面程序写的一个单元测试用例:
//请注意这个程序里面类名和方法名的特征
importjunit.framework.TestCase;
publicclass TestSample extends TestCase
{
public void testAdd()
{
SampleCalculatorcalculator = new SampleCalculator();
int result = calculator.add(50 , 20);
assertEquals(70 , result);
}
以test开头的方法为一个测试项。
集成测试
import junit.framework.Test;
importjunit.framework.TestSuite;
publicclass TestAll{
public static Test suite(){
TestSuite suite = new TestSuite("TestSuite Test");
suite.addTestSuite( TestSample.class);
return suite;
}
}
2、在android中的单元测试
基本上和是ava单元测试,不过要考虑到ui界面的选择等操作。
public class TestUtil extendsActivityInstrumentationTestCase2 {
// 声明一个Solo对象,Solo实例封装了所有Robotium的可用方法
privateSolo solo = null;
privatefinal int WAITING_SEARCH_AP_TIME = 10000; // 40S
// 声明一个Class类型的变量,用于ActivityInstrumentationTestCase2加载启动被测程序
privatestatic Class launcherActivityClass;
// 声明一个标签用于日志的输出控制,便于调试
finalString TAG = "shareMusicTest";
// 静态加载auncherActivityClass也就是被测程序主类
static{
try {
launcherActivityClass= Class
.forName("com.huaqin.wifi.apps.listen_music_together.TogetherListenerActivity");
}catch (ClassNotFoundException e) {
thrownew RuntimeException(e);
}
}
// 构造函数,传入TARGET_PACKAGE_ID,launcherActivityClass即可
publicTestUtil() {
super(
"com.huaqin.wifi.apps.listen_music_together.TogetherListenerActivity",
launcherActivityClass);
}
// 这个必须有,在测试用例初始时执行,我们在这里初始化了Solo实例
publicvoid setUp() throws Exception {
solo= new Solo(getInstrumentation(), getActivity());
}
// 这个也必须有,在测试用例执行完毕执行,我们在这里销毁了测试中建立的所有实例,清除垃圾
publicvoid tearDown() throws Exception {
solo.finishOpenedActivities();
}
public void testGetLocalHostName() throws Exception{
assertFalse(Util.getLocalHostName().isEmpty());
assertFalse(Util.getLocalHostName().equals(""));
assertFalse(Util.getLocalHostName().equals(""));
}
public void testGetFileExt() throws Exception {
Filefile1 = new File("/data/aaa.xml");
assertTrue(Util.getFileExt(file1).equals("xml"));
Filefile2 = new File("/data/bbb.mp3");
assertTrue(Util.getFileExt(file2).equals("mp3"));
file1.delete();
file2.delete();
}
public void testIsShowing()throws Exception {
Viewv = (View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.tune_wifi_quick); //一键收听按钮
assertTrue(Util.isShowing(v));
v=(View)solo.getCurrentActivity().findViewById(com.huaqin.wifi.apps.listen_music_together.R.id.wifi_hot_bg); //雷达view
assertFalse(Util.isShowing(v));
}
}
定义了三个测试项:testGetLocalHostName testGetFileExt testIsShowing
其中testIsShowing 使用solo 对象。
二、逻辑测试
逻辑测试可以自动模拟用户使用系统的情况。我们可以更具功能说明书和黑盒测试用例来编写逻辑测试代码。就是点击什么,进入什么界面,出现什么状态。
我这里采用Instrumentation.Android单元测试的主入口是InstrumentationTestRunner。它相当于JUnit当中TestRunner的作用。你可以将Instrumentation理解为一种没有图形界面的,具有启动能力的,用于监控其他类(用TargetPackage声明)的工具类。任何想成为Instrumentation的类必须继承android.app.Instrumentation。
public class TestWifiStatus extendsActivityInstrumentationTestCase2 {
// 声明一个Solo对象,Solo实例封装了所有Robotium的可用方法
privateSolo solo = null;
// 声明一个Class类型的变量,用于ActivityInstrumentationTestCase2加载启动被测程序
privatestatic Class launcherActivityClass;
// 声明一个标签用于日志的输出控制,便于调试
finalString TAG = "shareMusicTest";
// 静态加载auncherActivityClass也就是被测程序主类
static {
try{
launcherActivityClass= Class
.forName("com.huaqin.wifi.apps.listen_music_together.TogetherListenerActivity");
}catch (ClassNotFoundException e) {
thrownew RuntimeException(e);
}
}
// 构造函数,传入TARGET_PACKAGE_ID,launcherActivityClass即可
publicTestWifiStatus() {
super(
"com.huaqin.wifi.apps.listen_music_together.TogetherListenerActivity",
launcherActivityClass);
}
// 这个必须有,在测试用例初始时执行,我们在这里初始化了Solo实例
publicvoid setUp() throws Exception {
solo= new Solo(getInstrumentation(), getActivity());
}
// 这个也必须有,在测试用例执行完毕执行,我们在这里销毁了测试中建立的所有实例,清除垃圾
publicvoid tearDown() throws Exception {
solo.finishOpenedActivities();
}
}
下面给出几个测试用例
/主界面ui
publicvoid testUI() throws Exception {
Activityactivity = solo.getCurrentActivity();
assertTrue(solo.waitForText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.music_title))); //"Music Share"
assertTrue(solo.waitForText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.open_noaml_search))); //tune
assertTrue(solo.waitForText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.create_naomal_hot))); //Broadcast
View v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.phone_name); //大厅名称view
assertTrue(solo.waitForView(v));
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.tune_wifi); //“Tune”按钮view
assertTrue(solo.waitForView(v));
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.listener_music); //“Broadcast”按钮view
assertTrue(solo.waitForView(v));
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.tune_wifi_quick); //一键收听按钮view
assertTrue(solo.waitForView(v));
}
//我要收听:进入客户接分享界面
public void testTune() throws Exception {
Activityactivity = solo.getCurrentActivity();
solo.clickOnButton(activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.open_noaml_search)); //tune
assertTrue(solo.waitForText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.searching))); //"Searching"
//assertTrue(solo.waitForText("Searchingconnectable Share"));
assertTrue(solo.waitForText("正在搜索音乐服务器")); //"正在搜索音乐服务器"
View v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.wifi_hot_bg); //雷达view
assertTrue(solo.waitForView(v));
}
//我要分享:进入主机分享界面
public void testBroadcast() throws Exception {
Activity activity =solo.getCurrentActivity();
solo.clickOnButton(activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.create_naomal_hot)); //Broadcast
assertTrue(solo.searchText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.all_songs)));//"所有歌曲"
assertTrue(solo.searchText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.all_aritis)));//"艺术家"
assertTrue(solo.searchText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.all_album)));//"专辑"
assertTrue(solo.searchText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.defualt_song_playing)));//"Playing"
View v = (View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.playBtn); //播放按键view
assertTrue(solo.waitForView(v));
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.prevBtn); //上一首view
assertTrue(solo.waitForView(v));
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.nextBtn); //下一首view
assertTrue(solo.waitForView(v));
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.seekbar); //进度条view
assertTrue(solo.waitForView(v));
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.repeatBtn); //循环播放view
assertTrue(solo.waitForView(v));
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.shuffleBtn); //随机播放view
assertTrue(solo.waitForView(v));
}
//一键收听:搜索当前的热点,并自动加入第一个搜索到的非活动状态热点(大厅),进入U5界面等待主机播放,如果在规定时间内未搜索任何热点需有消息提示
public void testShotcut() throws Exception {
Activityactivity = solo.getCurrentActivity();
Viewv = (View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.tune_wifi_quick); //一键收听按钮
solo.clickOnView(v);
assertTrue(solo.waitForText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.searching))); //"Searching"
//assertTrue(solo.waitForText("Searchingconnectable Share"));
assertTrue(solo.waitForText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.now_quick_join)));//"正在搜索音乐服务器"
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.wifi_hot_bg); //雷达view
assertTrue(solo.waitForView(v));
solo.sleep(WAITING_SEARCH_AP_TIME);
//booleanbSearchNothing = solo.waitForText("error 提示",1,WAITING_SEARCH_AP_TIME);
booleanbSearchNothing = solo.waitForText(
activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.create_naomal_hot)); //Broadcast
assertTrue(bSearchNothing ||
solo.searchText(activity.getString(com.huaqin.wifi.apps.listen_music_together.R.string.title_tune))); //tune
v =(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.listbtn); //歌曲列表view
assertTrue(bSearchNothing|| solo.waitForView(v));
v=(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.albumIconBg); //CD view
assertTrue(bSearchNothing || solo.waitForView(v));
v=(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.seekbar); //进度条view
assertTrue(bSearchNothing|| solo.waitForView(v));
v= (View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.saveBtn); //保存按钮view
assertTrue(bSearchNothing || solo.waitForView(v));
v=(View)solo.getView(com.huaqin.wifi.apps.listen_music_together.R.id.musicIcon); //音乐icon view
assertTrue(bSearchNothing|| solo.waitForView(v));
}
这几个测试用例都是用的solo机器人,Robotium测试。
三、常见的Robotium测试的问题总结:
1、Robotium的测试类ActivityInstrumentationTestCase2继承了TestCase类,即robotiom的测试类是junit3的实例,并没有junit4的特征,比如通过annotate的方式来识别子类的新特征,没不能实现@beforeclass,@afterclass等特征。只能通过写setup和teardown,以及test开头的测试用例的方式进行测试case书写。
2、有些button没有string,没有text,只能通过index来click这样很不直观,而且button的index并不是固定的,有可能随着 控件重新加载,顺序也有可能发生变化,无法保证测试结果。查看了robotium源码,发现大多数click方法最终都是通过传入参数转成view,再调 用clickOnView,于是参照着写了一个通过button的ID来click的方法。Button的ID需要查看测试对象的源码中获取。比如导航中 就有菜单栏大多数据button就是这种类型的。
3、有的activity点击后不能获取焦点,可以通过另外的方式获取activity的内容,比如Activity act = solo.getCurrentActivity();获取当前的activity,然后通过act.findViewById的方式获取控件。
4、多个屏幕的情况,可以通过滚屏的方式滑动,solo.scrollToSide(Solo.LEFT),如果多屏属于一个activity,则不需要滑动也能运行case获取数据。
5、有时text view或者button的click方法会失效,咋办?答案是在被测程序的AndroidManifest.xml文件里加上这么一句:<supports-screens android:anyDensity="true"/>就行了。唉,当时为了找到这个解决方法可浪费了俺不少时间啊,最后在官网上找到答案了。
6、如果要想在robotium的测试程序里读写SD card肿么办?答案是在被测程序的AndroidManifest.xml文件里加上<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"></uses-permission>。注意是在被测程序里加上,在测试程序本身的manifest文件里加会很坑爹的。
7、listview动态添加item如何判断添加成功。可从添加前及添加后Item个 数判断,先确定添加item的属性,再通过相应的方法获取item。比如添加一个item可能需要三个textview,那么通过 getCurrentTextViews(View)前后获取到的个数差就相差三个。比如添加黑名单到黑名单列表。
(1)有的listView只有web,或者主要是文本,可以通过getItemAtPosition(i).toString()的方法获取第几行的内容。
solo.clickOnText(chooseProvPage.getListView().getItemAtPosition(i).toString());
(2)有的listView包含多个testView或者button,可以通过findViewById的方法得到某一行的一项的内容。
8、无法捕获Toast,这个有点不明白。大概实验了一下,可以使用waitForText这个函数来捕获文字,这个方法返回值是布尔型的,所以返回true就是找到了。
9、结果判断
(1)waitForText
该方法适用于点击操作后需要一点时间才返回结果的结果判断。比如联网操作,可以设置适当的延时,等待返回结果,判断结果更加正确。
(2)assertActivity
该方法适用于activity时,可以判断点击操作切换Activity是否正确,可以与waitfortext配合使用。
(3)searchText+assert
当有editText时,输入内容后,可通过searchText查找输入内容是否是预期结果,再将返回结果判断。
注:有些editText的内容无法通过searchText,原因暂时没找到。比如:手动添加黑名单时的名称及号码的输入框。
10、测试类要继承ActivityInstrumentationTestCase2<测试类类名>
11、构造方法中super("包名",测试类类名.class);
12、setUp方法中solo= new Solo(getInstrumentation(), getActivity());
13、tearDown方法中
try {
solo.finalize();
} catch (Throwable e) {
e.printStackTrace();
}
getActivity().finish();
super.tearDown();
14、点击自动化
clickOnMenuItem("菜单名")
clickInList(列表行数)注:从1开始
clickOnText("(?i).*?test.*") 点击文本
clickLongOnText("Note 2") 长时间点击文本
clickOnButton("按钮名")
点击按钮
15、输入自动化
enterText(号,"输入的内容")
16、屏幕控制
setActivityOrientation(Solo.LANDSCAPE或Solo.PORTRAIT)控制屏幕横向或纵向显示
17、跳转
goBack() 模仿硬返回键
goBackToActivity("Activity名")
跳到指定的Activity
18、判断
判断当前是否是指定的Activity
assertCurrentActivity("测试提示", "Activity名");
搜索指定文本是否存在
searchText("搜索文本")或searchText("(?i).*?note1 test")
后面这个是正则表达式
19、获取
(EditText) solo.getView(R.id.EditText01);
(TextView) solo.getView(R.id.TextView01);
ArrayList currentTextViews = solo.getCurrentTextViews(outputField);
20、点击按钮等测试中需要注意2点:
(1)真机测试时发现,屏保后点击按钮测试会报找不到该按钮,也就是点不中的意思,看来测试机器人还真仿真啊。
(2)点击按钮后有个延迟的过程,以后的测试需要循环等待一段时间,否则直接进入下面的测试后误报错错误,此处处理示例如下:
// 点击按钮开启服务
solo.clickOnButton(butStartService);
// 判断指定服务是否存在
long start = System.currentTimeMillis();
while (!isServiceStarted(SERVICE_PACKAGE_NAME)) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
if ((System.currentTimeMillis() - start) > TIMEOUT) {
break;
}
}
assertTrue("没有开启服务",isServiceStarted(SERVICE_PACKAGE_NAME));
21.
private Map<String,Integer> jdField_b_of_type_JavaUtilMap = new HashMap();
public View findViewById(String paramString) {
try {
int i;
if(this.jdField_b_of_type_JavaUtilMap.containsKey(paramString))
i = ((Integer) this.jdField_b_of_type_JavaUtilMap
.get(paramString)).intValue();
else
i = solo.getCurrentActivity()
.getResources()
.getIdentifier(paramString.replace(".R.id.",":id/"),
null, null);
if (i > 0) {
this.jdField_b_of_type_JavaUtilMap.put(paramString,
Integer.valueOf(i));
View localView1 = solo.getView(i);
if (localView1 != null)
return localView1;
ArrayList localArrayList = solo.getViews();
Iterator localIterator = localArrayList.iterator();
while (localIterator.hasNext()) {
View localView2 = (View) localIterator.next();
if (localView2.getId() == i)
return localView2;
}
} else {
return null;
}
} catch (Exception e) {
}
return null;
}