在android开发中,页面显示,交互部分,经常遇到一些情况,如手势操作,UI在特定环境下的显示,交互等等。
如何判断这些部分是否编码正确?
通常的办法是运行程序,直接看,操作。用实践来检验真理。简单,易行。
但是,这样在一些情况下是比较麻烦的,如上下文环境准备麻烦等等。
这里有一种简单的测试办法,用单元测试。
不过这里说的测试不是通常的自动化测试,而是一种交互式测试,直接显示页面,人工判断显示正确,操作正确等。
1,测试activity。在特定的数据环境下,页面显示是否正确。
这里举一个例子来说明:微信通讯录tab, “新的朋友”处的显示。
此处没有未读的“新的朋友”,有一条未读的记录,有多条未读的记录显示的情况是不一样的。
具体规则是:
没有未读,图片显示默认icon, 文字显示"新的朋友"
有一条未读,图片显示对方头像,文字为两行,显示对方姓名,邀请文字。最右边还有一个红色的未读图标。
有多条未读,显示对方的头像,最多显示4个,右边 显示数量。
//主页面大体结构
public class MainActivity extends Activity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
getDataAndBindView();
}
private void init(){
//初始化控件
}
private void getDataAndBindView(){
//这里假定是从数据库中获取数据
}
}
//测试代码大体结构
public class TestMainActivity extends ActivityInstrumentationTestCase2<MainActivity>{
private CountDownLatch countDownLatch = new CountDownLatch(1);
public TestMainActivity() {
super(MainActivity.class);
}
public void testNewContactOnEmpty() throws InterruptedException {
//1.初始化数据源,放入假数据。
//2.显示页面
MainActivity activity = getActivity();
//3.页面等待。并设置点击左上角的返回按钮后停止等待。
activity.findViewById(R.id.back).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
countDownLatch.countDown();
}
});
countDownLatch.await();
}
public void testNewContactOnOne(){
}
public void testNewContactOnThree(){
}
public void testNewContactOnFive(){
}
}
测试方法,可以在IDE中选择单个运行,来检验有效性。
在 getActivity() 方法调用后,手机上会显示测试中的Activity。
Activity会获取数据源中的数据(假数据),根据数据不同来显示不同的UI。
为什么要加锁来停住测试方法呢?
因为测试方法运行完,意味着此单测的完成,页面会关闭。不加锁的话,就会看到页面一闪而过。
所以,通过锁让测试方法等待,这样就有足够的时间来用你那勤劳的双手,像素双眼来检测是否正确。
检测完成后,点击页面左上角的返回,释放锁,测试方法结束。
因为需要你手动检测,所以叫她 交互式单元测试。不能自动化。千万别与自动化测试一起测,如加@SmallTest什么的。
当然,这个例子也是可以用自动化测试来搞定的,如assert 文字显示 assert 图片显示 等。
交互式测试有它的缺点,不能自动化。
也有它的优点,在开发中,一目了然的知道所有情况下的正确性,复杂UI交互时,在特定数据显示特定UI等等情况,能够大大缩短开发周期。
上面讲了activity的交互式单测,下面讲View的单元测试
2,测试View。在特定的数据环境下,View显示是否正确
这里举一个例子来说明: 锁屏时来电的滑动解锁。
锁屏时来电,下方会有滑动解锁的区域,点击接听按钮向右滑动的时候,整个背景会跟着缩短。
滑动到最右侧放开,会接听电话,否则会退回到原始状态。
//view 代码
public class AnswerCallBySlideView extends RelativeLayout{
private static final String TAG = "AnswerCallBySlideView";
private View vgDescription , vBtn , vgSlideArea;
private SlideListener slideListener;
public AnswerCallBySlideView(Context context) {
super(context);
init();
}
public AnswerCallBySlideView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public AnswerCallBySlideView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
inflate(getContext() , R.layout.view_slide_answer_call , this);
vgSlideArea = findViewById(R.id.slide_area);;
vBtn = findViewById(R.id.btn);
vgDescription = findViewById(R.id.description_ll);
vBtn.setOnTouchListener(slideTouchListener);
}
private OnTouchListener slideTouchListener = new OnTouchListener() {
private int maxWidth , minWidth;
private float startX;
private float diff;
private int error = 10;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:
maxWidth = vgSlideArea.getWidth();
minWidth = vBtn.getWidth() + vgSlideArea.getPaddingLeft()+vgSlideArea.getPaddingRight();
vgSlideArea.setMinimumWidth(minWidth);
startX = event.getRawX();
vgDescription.setVisibility(View.GONE);
LogUtil.i(TAG , "onTouch() down maxWidth="+maxWidth +",minWidth="+minWidth);
break;
case MotionEvent.ACTION_MOVE:
float curX = event.getRawX();
diff = curX - startX;
int newWidth = (int)(maxWidth-diff);
if(newWidth < minWidth){
newWidth = minWidth;
}
setWidth(newWidth);
LogUtil.i(TAG , "onTouch() move diff="+diff);
break;
case MotionEvent.ACTION_UP:
curX = event.getRawX();
diff = curX - startX;
if(maxWidth <= minWidth + diff + error){
notifySlideSuccess();
}else{
restore();
}
LogUtil.i(TAG , "onTouch() up ");
break;
case MotionEvent.ACTION_CANCEL:
restore();
LogUtil.i(TAG , "onTouch() cancel ");
break;
}
return true;
}
};
private void setWidth(int width){
ViewGroup.LayoutParams lp = vgSlideArea.getLayoutParams();
lp.width = width;
vgSlideArea.setLayoutParams(lp);
}
private void restore(){
vgDescription.setVisibility(View.VISIBLE);
ViewGroup.LayoutParams lp = vgSlideArea.getLayoutParams();
lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
vgSlideArea.setLayoutParams(lp);
}
private void notifySlideSuccess(){
LogUtil.i(TAG , "notifySlideSuccess()");
if(slideListener != null){
slideListener.onSlideSuccess();
}
}
public interface SlideListener{
public void onSlideSuccess();
}
public void setSlideListener(SlideListener slideListener) {
this.slideListener = slideListener;
}
}
先说明一下,这个不是源码,是我自己写的一段代码。
监听View的ontouch事件,来设定底部条的样子,当手指up时,通知成功,或者复原UI。
View要展示,需要放到一个Activitity中才行,所以写了一个测试View用的Activitiy。
package bj.lize.uitest;
import android.app.Activity;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import java.util.concurrent.CountDownLatch;
/**
* Created by lize on 2014/12/1.
*/
public class <span style="font-family: Arial, Helvetica, sans-serif;">TestViewActivity </span>extends Activity{
private CountDownLatch countDownLatch;
public LinearLayout vgContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
countDownLatch = new CountDownLatch(1);
vgContent = new LinearLayout(this);
vgContent.setOrientation(LinearLayout.VERTICAL);
vgContent.setBackgroundColor(0x000000);
setContentView(vgContent, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
@Override
public void onBackPressed() {
// super.onBackPressed();
unlock();
}
public void lock(){
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void unlock(){
countDownLatch.countDown();
}
public LinearLayout getContent() {
return vgContent;
}
}
很简单的一个Activity,顶级View是一个纵向的LinearLayout, 测试的时候向它里面放入待测试View。
提供了lock() unlock() 方法
package bj.lize.uitest;
import android.test.ActivityInstrumentationTestCase2;
/**
* Created by lize on 2014/12/14.
*/
public class TestAnswerCallBySlideView extends ActivityInstrumentationTestCase2<TestViewActivity>{
public TestAnswerCallBySlideView() {
super(TestViewActivity.class);
}
public void testUI(){
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
getActivity().getContent().addView(new AnswerCallBySlideView(getActivity()));
}
});
getActivity().lock();
}
}
执行了这个测试代码,就发现你的View出现在屏幕上了,你可以用你勤劳的双手双眼来确认它的正确性了。
之所以要用 getActivity().runOnUiThread 是因为测试方法并不是UI线程,如果直接执行 addView 操作,就会抛出异常。
到这里,交互式测试就讲完了,方法很简单。用到合适的场景,还是很爽的。
目前我经常用到的一些场景:
- 手势操作,就是需要写onTouch 的那些地方。
- 复杂数据显示:
Listview 显示数据,listitem有多种显示类型
不同数据有不同的显示UI
等等
大家还有什么好的想法,好的应用场景可以告诉我。谢谢。