Android 单元测试

来源:互联网 发布:python实例教程 编辑:程序博客网 时间:2024/06/05 16:27

先简单了解一下单元测试,对软件中的最小可测试单元进行测试,一般是函数。

接下来说说它的作用,(1)能够验证程序的准确性,为开发提供保障,能放心大胆的修改和重构。
(2)能规范我们的设计,能写单元测试的程序其耦合度更低。
(3)通过测试case,能很好的了解一个功能点涉及到的其他隐藏功能点,从这一点上来看是很好的文档。
好了,上面说到了这么多好处,那么开发人员有多少人写单元测试呢?嗯,大约58.6的人, 基本上不写单元测试,还有16.6的人从来不写单元测试。
再来说说Android,ios这类GUI程序,由于有很多交互行为,耦合度较高,写单元测试的难度大大增加(从谷歌的一份demo中看到纯java的工具类测试
覆盖率可以到60以上,但是Android部分基本在10-30之间),估计写的人就更少了。

写的人虽然少,但是单元测试还是很有意义的,下面开始我们的Android单元测试之旅。

首先,来认识一下UT(单元测试,以下用UT替换)相关的概念和框架,由于资料的混杂和对UT的不熟悉,一开始真是把我整懵逼了。
(1)JUnit(推荐使用JUnit4)
java语言的UT框架。
(2)AndroidJUnitRunner:是一个测试运行器,用于运行JUnit的Android测试包。
(3)Roboletric:比Instrumenttation更强大,运行更快的Android自动化测试工具类 ,只在JVM中运行。
(4) Mockito:一个测试框架,用于解耦程序中耦合度较高的部分。
(4)Espresso:Google推出的Instrumentation UI测试框架,API支持丰富。
(5)Instrumentation:早期Google提供的用于Android的自动化测试工具类,可以测试按键点击,滚动等Android相关问题。
说明:经过实践比对,我们最终用到的是前4个,后两个可以了解一下。

下面开始实战,从最简单的入手,用JUnit测试一个工具类。
环境部署:AS中集成了JUnit框架,无须额外部署。
(1)我们先创建一个被测试类,util.class,其中有一个读取文件的方法,我们接下来就测试这个方法。

public class util {    public static String readTxtFile(String filePath){            String encoding="utf-8";            File file=new File(filePath);            StringBuilder sb = new StringBuilder();            if(file.isFile() && file.exists()){ //判断文件是否存在                InputStreamReader read = null;//考虑到编码格式                try {                    read = new InputStreamReader(                            new FileInputStream(file),encoding);                } catch (UnsupportedEncodingException e) {                    e.printStackTrace();                } catch (FileNotFoundException e) {                    e.printStackTrace();                }                BufferedReader bufferedReader = new BufferedReader(read);                String lineTxt = null;                try {                    while((lineTxt = bufferedReader.readLine()) != null){                        sb.append(lineTxt);                        System.out.println(lineTxt);                    }                } catch (IOException e) {                    e.printStackTrace();                }                try {                    read.close();                } catch (IOException e) {                    e.printStackTrace();                }                return sb.toString();            }else{                System.out.println("找不到指定的文件");                return null;            }    }}

(2)然后打开这个类,右键 → go to → Test → Create new Test…
setUp@Before :用以unitTest前的数据初始化等。生成的Test类会有一个@Before注解的setUp方法。
tearDown@After :用以unitTest后的垃圾回收等操作。生成的Test类会有一个@After注解的tearDown方法。
然后在setUp方法中new 一个util对象。
然后就可以在@Test注解的方法中愉快的写测试case了。
怎么写呢?
这里可以用assertEquals(), 第一个参数是期望值,第二个参数是实际值。 JUnit框架还有很多其他的方法。

assertEquals(null , util.readTxtFile(null));

好,然后我们来运行一下,右键run。看结果:
这里写图片描述
嗯,错了,报了NullPoint错误,我们来修改一下方法,加一个判空。
在运行,绿色的读条,嗯,对了。

很简单是不是,那下面开始Android的测试。
开始之前先说一下为什么我们选择Roboletric而不是Espresso,因为前者是在jvm上运行的,快啊;后者还是需要在模拟器或者真机上跑一次的,
想一下AS那编译速度,知道为什么选前者了吧。

环境配置:

testCompile'org.robolectric:robolectric:3.0'androidTestCompile 'com.android.support:support-annotations:23.4.0'    androidTestCompile 'com.android.support.test:runner:0.4'    androidTestCompile 'com.android.support.test:rules:0.4'

到这儿,你应该已经出错了:
Conflict with dependency ‘com.android.support:support-annotations’. Resolved versions for app (23.1.0) and test app (23.0.1) differ)
第一种解决方法:androidTestCompile ‘com.android.support:support-annotations:23.1.0’
第二种解决方法:在Top Level Gradle文件中配置所有(如果还有其他地方用到了annotations,第一种方法就又会报错):
allprojects {
configurations.all {
resolutionStrategy.force ‘com.android.support:support-annotations:23.4.0’
}
}
参考:http://stackoverflow.com/questions/33317555/conflict-with-dependency-com-android-supportsupport-annotations-resolved-ver

解决了上面的问题后我们继续:
我们创建一个LoginActivity,输入手机号,密码,点击登陆后先验证手机号和密码,如果符合规则,进度条就显示,并开始登陆。
先看一下我们的验证程序:

    /*check rule*/    public boolean checkRule(String phone , String password){        Pattern pattern = Pattern.compile("^1[3|4|5|7|8][0-9]\\d{8}]$");        if(phone == null)            return false;        Matcher matcher = pattern.matcher(phone);        if(!matcher.find()) {            Toast.makeText(mContext , "手机号格式不正确!" , Toast.LENGTH_SHORT).show();            return false;        }        if("".equalsIgnoreCase(password)){            Toast.makeText(mContext , "密码不能为空!" , Toast.LENGTH_SHORT).show();            return false;        }        return true;    }

先开一下数据和控件的初始化:

    @Before    public void setUp() throws Exception {        loginActivity = Robolectric.setupActivity(LoginActivity.class);        mEtPwd = (EditText) loginActivity.findViewById(R.id.activity_login_et_pwd);        mEtUser = (EditText) loginActivity.findViewById(R.id.activity_login_et_user);        mProgressBar = (ProgressBar) loginActivity.findViewById(R.id.activity_login_progressbar);        mTvLogin = (TextView) loginActivity.findViewById(R.id.activity_login_tv_login);    }

然后是测试验证程序,注意Toast弹出的测试:

    @Test    public void testCheckRule() throws Exception {       // assertEquals(true , loginActivity.checkRule("13992758986" , "123"));        assertEquals(false , loginActivity.checkRule("13992758986" , ""));        assertEquals(false , loginActivity.checkRule("13992758986" , null));        assertEquals(false , loginActivity.checkRule("1399275898" , "wang123"));        assertEquals(false , loginActivity.checkRule("10992758986" , "wang123"));        assertEquals(false , loginActivity.checkRule("" , "wang123"));        assertEquals(false , loginActivity.checkRule(null , "wang123"));        loginActivity.checkRule("123" , "");        assertEquals("手机号格式不正确!" , ShadowToast.getTextOfLatestToast());        assertEquals(false , loginActivity.checkRule("109927589861" , "wang123"));        assertEquals(false , loginActivity.checkRule("13992758*86" , "wang123"));        assertEquals(false , loginActivity.checkRule("139927&^%%#" , "wang123"));    }

然后是点击登陆按钮,注意进度条的显隐测试:

    public void testLogin() throws Exception {        mEtUser.setText("18380173957");        mEtPwd.setText("wang123");        assertEquals(View.VISIBLE , mProgressBar.getVisibility());        mTvLogin.performClick();        assertEquals(View.VISIBLE , mProgressBar.getVisibility());    }

以上只是简单的测试了Toast的弹出,控件的隐藏显示等。其他诸如Fragment,广播,资源文件访问,Activity跳转等
只是不同的方法,查看具体API即可。

前面讲的都是耦合度比较低的情况,那么当耦合度比较高,比如网络调用怎么办呢?结果回调里面的数据怎么模拟呢?

这就需要另外一个框架来解耦了:mockito
环境搭建:testCompile “org.mockito:mockito-core:1.+”
先看被测试类,是一个OkHttp调用网络的例子,写正常情况下是不会这样写的,先别问为什么这样,代码如下:

public class CallbackTestActivity extends AppCompatActivity {    public RequestCall call;    public TextView mTvTest;    public String result;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_callback_test);        call = OkHttpUtils.get()                .url("http://120.132.7.14:8080/lanmao/api/register.php")                .addParams("phone" , "1")                .build();        mTvTest = (TextView) findViewById(R.id.activity_callback_tv_test);        mTvTest.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                register();            }        });    }    /*设置单元测试传递过来的call*/    public void setCall(RequestCall call){        this.call = call;    }    /*点击调用网络*/    public void register(){        call.execute(new Callback() {            @Override            public Object parseNetworkResponse(Response response, int id) throws Exception {                return null;            }            @Override            public void onError(Call call, Exception e, int id) {            }            @Override            public void onResponse(Object response, int id) {                result = response.toString();            }        });    }}

测试类必须加注解:
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
然后我们mock一个RequestCall对象(用注解实现)。
将上一步创建的RequestCall对象set到被测试类中。
完整代码如下:

@RunWith(RobolectricGradleTestRunner.class)@Config(constants = BuildConfig.class, sdk = 21)public class CallbackTestActivityTest {    private CallbackTestActivity callbackTestActivity;    @Mock    private com.zhy.http.okhttp.request.RequestCall call;    @Captor    private ArgumentCaptor<com.zhy.http.okhttp.callback.Callback> callbackArgumentCaptor;    @Before    public void setUp() throws Exception {        MockitoAnnotations.initMocks(this);        callbackTestActivity = Robolectric.setupActivity(CallbackTestActivity.class);        callbackTestActivity.setCall(call);    }    @Test    public void testCallback(){        /*开始请求*/        callbackTestActivity.register();        /*假结果*/        final String result = "hello , world";        Mockito.verify(call, Mockito.times(1)).execute(callbackArgumentCaptor.capture());        assertEquals(callbackTestActivity.result , null);        callbackArgumentCaptor.getValue().onResponse(result , 1);        assertEquals(callbackTestActivity.result , result);    }}

现在再来看为什么我们的被测试类要将RequestCall单独new出来,再execute了,因为需要在测试类中传一个mock的RequestCall对象回去。
同时我们还提供了一个set方法。
所以这里暴露了一个缺点,就是侵入性很强,还有一个体现点是要测试的方法需要设置为public。

到这里,一个完整的单元测试教程就完了。里面的框架都只举了一个例子,但是难度比较大的配置和使用问题都解决了,另外API数量
众多,需要在实际使用中学习。

总结:单元测试好处有很多,但是也有一些弊端,比如侵入性强,工作量比较大(一般是被测试代码的3倍),Android等GUI程序测试覆盖率不高等。在实际环境中应灵活选择。希望更多的开发者重视并开始对项目做单元测试。

附录:
(1)美团点评技术团队的一篇博客,里面讲了单元测试的设计流程,可以作为学习单元测试的大纲:
http://tech.meituan.com/Android_unit_test.html
(2)学习 Robolectric其他API用法的博客:
http://www.jianshu.com/p/9d988a2f8ff7

0 0
原创粉丝点击