前言
- 学习一样东西的时候,从是什么开始,然后会去探究为什么以及这么做之后的好处是什么,当经历了这个过程之后。才能够说是知道这个东西。
- 笔者在刚开始接触单元测试的时候,只知道这个是测试用的,只知道调用这个类的这个方法是返回这个东西的,但是不知道怎么去测,为什么要去这样子测,会觉得增加代码量和工作负担。
- 这个时候,就会被卡主。但是,如果是从更高一点的视角去看的话,就会知道怎么去测,我为什么要这样子去测。
- 开发一个功能,首先从需求的提出,然后针对这个需求设计各种使用的用例。我们开发出一个功能无非就是为了满足用户各种使用场景的需求,根据各种不同的使用场景产生各种结果。
- 我们身为开发者,所写的一些代码也是为了实现功能。
- 如果可以在开发的同时,可以顺便把每个用户的使用场景都测试一遍,那该是多么爽的事情啊。这样子写出来的代码就有依据。知道是
Cover
了什么的一些测试用例。 - Yeah! We have a thing for the shit! It is call the Test Drive Development(TDD)。It is born for this!
没有测试之前
测试Pass之后
准备
新建类以及接口
LoginContract
LoginPresenter
LoginActivity
LoginPresenter和LoginActivity各自实现了自己的接口,在上面的接口中,我们是没有定义任何一种方法,只是定义了一个模板。里面具体的方法都由接下来的用例所决定。
对于登录这个功能来说,会有以下的测试用例:
用户名为空的时候,显示错误信息
密码为空的时候,显示错误信息
用户名和密码输入成功,登录验证成功后进行跳转
用户名和密码有输入,但是验证不通过,显示错误信息
接下来,根据上面的测试用例,我们开始写我们的单元测试代码
下面只会选取第一个测试用例进行讲解,完整的例子会分享在github上,戳这里
Go For the TDD
前期准备
加入Mockito
testCompile 'org.mockito:mockito-core:2.5.4'
在MVP中,业务逻辑都是写在Presenter中,所以这里我们对LoginPresenter进行一个测试。右键点击LoginPresenter自动生成对应的测试类。
在LoginPresenterTest类中的setup方法里做一些初始化的操作
public class LoginPresenterTest {
private LoginContract.View mView;
private Api api;
private LoginPresenter mPresenter;
@Before
public void setUp() throws Exception {
mView = mock(LoginContract.View.class);
api = mock(Api.class);
mPresenter = new LoginPresenter(mView, api);
}
}
主要是初始化presenter,presenter需要注入两个对象来完成初始化。
我们把需要注入的两个对象都Mock掉,被Mock掉的对象我们就可以自由操控它,让他返回我们希望要的值。因为我们这里测试的是LoginPresenter,所以,与这个无关的对象的具体逻辑我们不用去关注。
关于Mockito的更多用法可以参考这篇文章Android单元测试(四):Mock以及Mockito的使用
测试第一个用例(用户名为空的时候,显示错误信息)
在LoginPresenterTest中新增一个@Test方法,新增后的代码如下
public class LoginPresenterTest {
private LoginContract.View mView;
private Api api;
private LoginPresenter mPresenter;
@Before
public void setUp() throws Exception {
mView = mock(LoginContract.View.class);
api = mock(Api.class);
mPresenter = new LoginPresenter(mView, api);
}
@Test
public void testWhenTheUserNameIsEmpty() throws Exception {
}
}
接下来,我们要为这个测试用例写上一些具体的测试逻辑,并感受TDD的美妙
首先,来写一个伪“中文代码”
presenter调用处理具体逻辑的方法
验证当用户名为空的时候,mView有没有调用到我们期望的方法
以上就是这个测试用例对应的测试逻辑,是不是觉得上面这两句话没啥用,说了相当于没说。
我们只需要去写针对当前测试用例的测试逻辑,并验证。
但是我觉得这才是单元测试美妙的地方,他可以把测试粒度分得很小,以至于我们能够cover到每个case。
No code say a ***, show me the fucking source code
我们可以看到,很多方法是没有的(上图红色的),我们只需要关注我们需要调用到哪一些方法,只需要关注我们的逻辑,一些模板代码让IDE帮我们生成。
按下快捷键(mac是option+enter
, windows是alt+enter
)
最终的结果就是,在LoginContract
中的对应的接口中自动生成了我们想要的方法。
最后,在我们实现了这个接口的类中实现这些方法,在给每个方法写上具体的逻辑。
最终LoginActivity
如下
最终LoginPresenter
如下
以上就是完成了我们第一个测试用例的业务逻辑,接下来就Run
一下我们的测试方法,如果能够Pass
,说明我们已经Cover
了这个用例。那么就可以继续开始写下一个用例的测试方法,然后再去完善我们的逻辑代码,再Run
一下我们新的用例的测试方法~如此循环。最后达到Cover
每个测试用例的目的。
看到绿色的真高兴,说明我们的第一个测试用例已经通过了。
接下来只需要重复上面的步骤,把剩下的测试用例Cover
到了就ok
总结
个人觉得在写单元测试的时候,最难的地方在于测试用例的择写。
在看了前面的例子之后,有人会疑惑,我为什么要把获取用户名和获取用户密码的那一部分抽出一个方法getInputUsername()
和getInputPwd()
,为什么不直接把用户名和密码当做参数传给presenter的方法。
这样子做的原因如下:
- 对Mock对象有一个更加深刻的理解,笔者刚接触单元测试的时候,基本上都是被Mock对象给搞晕了,特别是类似这样子的代码
when(mView.getInputUsername()).thenReturn("louiszgm");
一开始不知道这样子做的意义是什么,为什么要手动指定返回值,只知道这个方法是指定了Mock对象的特定方法返回了一个特定的值。
这个例子主要是用来去感受TDD的好处,很多地方都没有去完善,比如:
- 在正式项目中,MVP的使用当然没有这么简陋,需要考虑
view
的泄漏之类的问题,也会根据每个公司的业务抽出一些基接口
。- Mockito的一些高级用法
- 结合Dagger2让单元测试来的更爽
- 这些已经有人写了很多优质的文章,又在这里打一波广告。关于单元测试框架的使用,推荐创神的单元测试系列的文章。
Android 单元测试: 首先,从是什么开始
个人感觉用TDD这个模式去开发的好处是:
对自己写出来的代码有信心,毕竟cover到了每个用例。
很多模板代码都不用自己写,让关注点更多的关注到我们的逻辑