Android 单元测试项目集成(Junit + Mockito + Robolectric + JaCoCo)

一.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 = new ArrayList();
user.add(test1);
user.add(test2);
                  
//测试集合中是否还有指定元素
assertThat(user, hasItem(test1));
assertThat(user, hasItem(test2));
  
/**Map匹配**/
Map userMap = new HashMap();
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
Android 单元测试项目集成(Junit + Mockito + Robolectric + JaCoCo)_第1张图片
  • 使用方式:这里就不去详细介绍每一个控件的测试方法,就以一个实际例子介绍下
public class LoginActivity extends BaseActivity 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, Scheduler>() {
                    @Override
                    public Scheduler apply(Callable scheduler) throws Exception {
                        return immediate;
                    }
                });
                RxJavaPlugins.setInitComputationSchedulerHandler(new Function, Scheduler>() {
                    @Override
                    public Scheduler apply(Callable scheduler) throws Exception {
                        return immediate;
                    }
                });
                RxJavaPlugins.setInitNewThreadSchedulerHandler(new Function, Scheduler>() {
                    @Override
                    public Scheduler apply(Callable scheduler) throws Exception {
                        return immediate;
                    }
                });
                RxJavaPlugins.setInitSingleSchedulerHandler(new Function, Scheduler>() {
                    @Override
                    public Scheduler apply(Callable scheduler) throws Exception {
                        return immediate;
                    }
                });
                RxAndroidPlugins.reset();
                RxAndroidPlugins.setMainThreadSchedulerHandler(new Function() {
                    @Override
                    public Scheduler apply(Scheduler scheduler) throws Exception {
                        return immediate;
                    }
                });
                RxAndroidPlugins.setInitMainThreadSchedulerHandler(new Function, Scheduler>() {
                    @Override
                    public Scheduler apply(Callable 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
Android 单元测试项目集成(Junit + Mockito + Robolectric + JaCoCo)_第2张图片
测试报告

四.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
Android 单元测试项目集成(Junit + Mockito + Robolectric + JaCoCo)_第3张图片

其他:

  • 1.配置日志输出
 unitTests.all{
            testLogging {
                events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
                outputs.upToDateWhen { false }
                showStandardStreams = true
            }
        }

总结:

单元测试本身是对代码质量的一种把控,当我们case越多,覆盖的代码率越高,出现异常的情况就会越少。以上中P层的代码更加注重代码的逻辑,所以验证时以View层是否被调用为准;View层以View的变化为准,比如是否弹出正确toast、某一个控件的String是否发生变化、Activity是否跳转等等

参考连接:
Android单元测试研究与实践
Robolectric使用教程

你可能感兴趣的:(Android 单元测试项目集成(Junit + Mockito + Robolectric + JaCoCo))