Android 单元测试项目集成(Junit + Mockito
一.JUnit
Java自带的单元测试工具,用于m跟p层的单元测试,需要了解一些注解
@Before
@After
@Test
等
集成方式
testImplementation 'junit:junit:4.12'
- 关于JUnit的断言。
assertTrue 判断是否为true。
assertFalse 判断是否为false。
assertSame 判断引用地址是否相等。
assertNotSame 判断引用地址是否不相等。
assertNull 判断是否为null
assertNotNull 判断是否不为null
assertEquals 判断是否相等
assertNotEquals 判断是否不相等
assertThat 条件判断断言
上边说的assertThat
,下边详细介绍下
/**数值匹配**/
//测试变量是否大于指定值
assertThat(test1.getShares(), greaterThan(50));
//测试变量是否小于指定值
assertThat(test1.getShares(), lessThan(100));
//测试变量是否大于等于指定值
assertThat(test1.getShares(), greaterThanOrEqualTo(50));
//测试变量是否小于等于指定值
assertThat(test1.getShares(), lessThanOrEqualTo(100));
//测试所有条件必须成立
assertThat(test1.getShares(), allOf(greaterThan(50),lessThan(100)));
//测试只要有一个条件成立
assertThat(test1.getShares(), anyOf(greaterThanOrEqualTo(50), lessThanOrEqualTo(100)));
//测试无论什么条件成立(还没明白这个到底是什么意思)
assertThat(test1.getShares(), anything());
//测试变量值等于指定值
assertThat(test1.getShares(), is(100));
//测试变量不等于指定值
assertThat(test1.getShares(), not(50));
/**字符串匹配**/
String url = "http://www.taobao.com";
//测试变量是否包含指定字符
assertThat(url, containsString("taobao"));
//测试变量是否已指定字符串开头
assertThat(url, startsWith("http://"));
//测试变量是否以指定字符串结尾
assertThat(url, endsWith(".com"));
//测试变量是否等于指定字符串
assertThat(url, equalTo("http://www.taobao.com"));
//测试变量再忽略大小写的情况下是否等于指定字符串
assertThat(url, equalToIgnoringCase("http://www.taobao.com"));
//测试变量再忽略头尾任意空格的情况下是否等于指定字符串
assertThat(url, equalToIgnoringWhiteSpace("http://www.taobao.com"));
/**集合匹配**/
List<User> user = new ArrayList<User>();
user.add(test1);
user.add(test2);
//测试集合中是否还有指定元素
assertThat(user, hasItem(test1));
assertThat(user, hasItem(test2));
/**Map匹配**/
Map<String,User> userMap = new HashMap<String,User>();
userMap.put(test1.getUsername(), test1);
userMap.put(test2.getUsername(), test2);
//测试map中是否还有指定键值对
assertThat(userMap, hasEntry(test1.getUsername(), test1));
//测试map中是否还有指定键
assertThat(userMap, hasKey(test2.getUsername()));
//测试map中是否还有指定值
assertThat(userMap, hasValue(test2));
关于匹配的字符串详情点击
二.Mockito
所谓的mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:
1.验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
2.指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作
集成方式
testImplementation 'org.mockito:mockito-core:2.23.0'
使用
- 验证方法调用次数
User user = Mockito.mock(User.class);
UserManager manager = new UserManager(user);
manager.login("xmq","123456");
Mockito.verify(user,Mockito.times(1)).login(Mockito.anyString(),Mockito.anyString()); //验证User中的login调用了多少次
private class User {
public void login(String user, String pass) {
System.out.print(user+pass);
}
}
private class UserManager {
private User mUser;
public UserManager(User user) {//这里注意下,对象是以一种注入的方式
mUser = user;
}
public void login(String user, String pass) {
mUser.login(user,pass);
}
}
- 指定mock对象的某些方法的行,或者是执行特定的动作
User user = Mockito.mock(User.class);
Mockito.when(user.isMaster("xmq")).thenReturn(true);
Assert.assertTrue(user.isMaster("xmq"));
class User {
public void login(String user, String pass) {
System.out.print(user+pass);
}
public boolean isMaster(String user) {
return "xmq".equals(user);
}
}
注意:这里有个问题,若删除
Mockito.when(user.isMaster("xmq")).thenReturn(true);
这一行的话isMaster
方法本身传入参数为xmq
时正常逻辑返回true
,可是实际上是false
。这是因为mock如果不指定返回值的话,一个mock对象的所有非void方法都将返回默认值:int、long类型方法将返回0,boolean方法将返回false,对象方法将返回null等等;而void方法将什么都不做。
替代方案,使用Spy
,spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值.
User user = Mockito.spy(User.class);
User user = Mockito.spy(new User());
List list = new LinkedList();
List spy = spy(list);
//下边两种处理是不一样的
doReturn("foo").when(spy).get(0); //返回的是 foo
when(spy.get(0)).thenReturn("foo"); //将会抛出 IndexOutOfBoundsException 的异常,因为 List 为空
三.Robolectric
用于View层的单元测试,可直接运行于JVM上,其实内部是使用了一个android.jar包,具体原理有时间再理
- 集成方式
build.gradle 中
android{
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
testImplementation "org.robolectric:robolectric:3.8" //这里4.0以上的需要AndroidStudio 3.2以上才可以
testImplementation "org.robolectric:robolectric-annotations:3.4-rc2"
注意:若出现AndroidManifest.xml找不到的时候在Edit configurations 中配置Working directory 配置为$MODULE_DIR$
edit configurations -> defaults -> android junit -> working directory选择$MODULE_DIRS
- 使用方式:这里就不去详细介绍每一个控件的测试方法,就以一个实际例子介绍下
public class LoginActivity extends BaseActivity<LoginContract.LoginPresenter> implements LoginContract.LoginView {
@BindView(R.id.tv_login_user_id)
EditText etUserId;
@BindView(R.id.tv_login_user_pass)
EditText etUserPass;
@BindView(R.id.tv_user_name)
TextView tvUserName;
@Override
protected int getLayoutId() {
return R.layout.login_activity;
}
@Override
protected void init() {
}
@OnClick(R.id.btn_login)
public void login() {
//view 可以进行一些简单的逻辑处理,比如盼空校验等,就没必要交给presenter了
if (TextUtils.isEmpty(etUserId.getText())) {
showToast(getString(R.string.login_user_empt));
return;
}
if (TextUtils.isEmpty(etUserPass.getText())) {
showToast(getString(R.string.login_pass_empy));
return;
}
presenter.login(etUserId.getText().toString(), etUserPass.getText().toString());
}
@Override
protected LoginContract.LoginPresenter createPresenter() {
return new LoginPresenter(new LoginSource());
}
@Override
public void loginSuccess() {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
@Override
public void loginFail() {
//登录失败后,可以清空账号 密码 之类的UI操作
etUserPass.setText("登录失败!");
}
}
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 26)
public class LoginActivityTest {
private EditText etUserId;
private EditText etUserPass;
private Button btnLogin;
private LoginActivity mLoginActivity;
@Rule
public RxJavaTestSchedulerRule mRxJavaTestSchedulerRule = new RxJavaTestSchedulerRule(); //增加Rxjava规则
@Before
public void setUp() throws Exception {
mLoginActivity = Robolectric.buildActivity(LoginActivity.class).setup().get(); //创建Activity
etUserId = mLoginActivity.findViewById(R.id.tv_login_user_id); //获取其中的控件
etUserPass = mLoginActivity.findViewById(R.id.tv_login_user_pass);
btnLogin = mLoginActivity.findViewById(R.id.btn_login);
}
@After
public void tearDown() throws Exception {
}
@Test
public void login() {
etUserId.setText("xmq");
etUserPass.setText("123456");
btnLogin.performClick(); //Button的点击事件
assertEquals("登录失败!", ShadowToast.getTextOfLatestToast()); //断言是否弹出“ 登录失败!”toast
etUserId.setText("xuser");
etUserPass.setText("Zc123456");
btnLogin.performClick();
assertEquals("登录成功!", ShadowToast.getTextOfLatestToast());
}
@Test
public void loginSuccess() {
mLoginActivity.loginSuccess();
Intent expectedIntent = new Intent(mLoginActivity, MainActivity.class);
Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();
assertEquals(expectedIntent.getComponent(), actualIntent.getComponent()); //断言Activity跳转是否正确
}
@Test
public void loginFail() {
mLoginActivity.loginFail();
assertEquals("登录失败!",etUserPass.getText().toString());
}
}
关于RxJavaTestSchedulerRule 规则是将Rxjava异步转为同步
public class RxJavaTestSchedulerRule implements TestRule {
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
// ShadowLog.stream = System.out;
LcHttpClientWrapper.getInstance().sync(true);
RxJavaPlugins.reset();
final Scheduler immediate = new Scheduler() {
@Override
public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
return super.scheduleDirect(run, 0, unit);
}
@Override
public Worker createWorker() {
return new ExecutorScheduler.ExecutorWorker(new Executor() {
@Override
public void execute(@android.support.annotation.NonNull Runnable runnable) {
runnable.run();
}
});
}
};
RxJavaPlugins.setInitIoSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
RxJavaPlugins.setInitComputationSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
RxJavaPlugins.setInitNewThreadSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
RxJavaPlugins.setInitSingleSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
RxAndroidPlugins.reset();
RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return immediate;
}
});
RxAndroidPlugins.setInitMainThreadSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
base.evaluate();
}
};
}
}
自定义shadow:
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
}
@Implements(User.class) //增加关联注解
public class ShadowUser {
@Implementation //重写的方法
public String getName() {
return "shadowXmq";
}
}
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class , shadows = ShadowUser.class,sdk= 26) //这里需要使用shadow关联shadow对象
public class ShadowTest {
@Test
public void name() {
User user =new User("xmq");
assertEquals("xmq",user.toString());
assertNotEquals("xmq",user.getName());
}
}
Roboletric详情点击
- 生成报告
./gradlew clean testDebugUnitTest
测试报告
四.JaCoCo
使用JaCoCo生成测试报告,Android Instrument Test 中默认已经集成,但是在Android Unit Test并没有集成,需要我们手动配置gradle
- 使用方式
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.0" //指定jacoco的版本
reportsDir = file("$buildDir/JacocoReport") //指定jacoco生成报告的文件夹
}
android {
buildTypes {
debug {
//打开覆盖率统计开关
testCoverageEnabled = true
}
}
}
//依赖于testDebugUnitTest任务
task jacocoTestReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {
group = "reporting" //指定task的分组
reports {
xml.enabled = true //开启xml报告
html.enabled = true //开启html报告
}
def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug/com/*/*/*", //指定类文件夹, 这里的路径需要指定你的包名
includes: ["**/*.*"], //包含类的规则,这里我们生成所有Presenter类的测试报告
excludes: ['**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*']) //排除类的规则
def mainSrc = "${project.projectDir}/src/main/java" //指定源码目录
sourceDirectories = files([mainSrc])
classDirectories = files([debugTree])
executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec") //指定报告数据的路径
}
执行代码生成报告:
./gradlew clean jacocoTestReport
其他:
- 1.配置日志输出
unitTests.all{
testLogging {
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
outputs.upToDateWhen { false }
showStandardStreams = true
}
}
总结:
单元测试本身是对代码质量的一种把控,当我们case越多,覆盖的代码率越高,出现异常的情况就会越少。以上中P层的代码更加注重代码的逻辑,所以验证时以View层是否被调用为准;View层以View的变化为准,比如是否弹出正确toast、某一个控件的String是否发生变化、Activity是否跳转等等