Android单元测试(八):Dagger与单元测试

920 阅读4分钟

本篇紧接着上一篇的内容,对MVP + Dagger进行单元测试。Dagger的部分可以参看 Dagger2与AndroidInjector

1.相关实现代码

首先添加Dagger所需的依赖:

compile 'com.google.dagger:dagger:2.13'
compile 'com.google.dagger:dagger-android:2.13'
compile 'com.google.dagger:dagger-android-support:2.13'
annotationProcessor 'com.google.dagger:dagger-compiler:2.13'
annotationProcessor 'com.google.dagger:dagger-android-processor:2.13'

例子还是使用登录场景的获取验证码与登录。与上篇重复的代码我就忽略了。

封装Dagger的Avtivity : BaseMVPDaggerActivity

public abstract class BaseMVPDaggerActivity<V extends MvpView, T extends BaseMVPPresenter<V>> extends DaggerAppCompatActivity implements MvpView {

    @Inject
    protected T mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter.attachView((V)this);
        mProgress = new ProgressDialog(this);
    }
    ...
}

BaseMVPPresenterMvpView不变。

GithubApi的初始化。

@Module
public class ClientModule {

    @Singleton
    @Provides
    GithubApi provideGithubApi(Retrofit retrofit){
        return retrofit.create(GithubApi.class);
    }

    @Singleton
    @Provides
    Retrofit provideRetrofit(Retrofit.Builder builder, OkHttpClient client) {
        return builder
                .baseUrl(HttpUrl.parse(GithubApi.BASE_URL))
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }

    @Singleton
    @Provides
    OkHttpClient provideClient(OkHttpClient.Builder okHttpClient, LoggingInterceptor loggingInterceptor) {
        OkHttpClient.Builder builder = okHttpClient.addInterceptor(loggingInterceptor);
        return builder.build();
    }

    /**
     * 打印信息的拦截器
     * @return 拦截器
     */
    @Singleton
    @Provides
    LoggingInterceptor provideLoggingInterceptor() {
        return new LoggingInterceptor(); 
    }

    @Singleton
    @Provides
    Retrofit.Builder provideRetrofitBuilder() {
        return new Retrofit.Builder();
    }

    @Singleton
    @Provides
    OkHttpClient.Builder provideClientBuilder() {
        return new OkHttpClient.Builder();
    }

}

有了ClientModule,我们就可以轻松地获取到GithubApi来随时随地的调用网络接口了。

自定义MyApp 继承DaggerApplication

public class MyApp extends DaggerApplication {

    private static MyApp instance;

    @Inject
    GithubApi mGithubApi; 

    @Override
    public void onCreate() {
        super.onCreate();

        if (instance == null){
            instance = this;
        }
    }

    public static MyApp getInstance() {
        return instance;
    }
    //直接获取,便于测试
    public GithubApi getGithubApi(){
        return mGithubApi;
    }

    @Override
    protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
        return DaggerAppComponent.builder().create(this);
    }
}

如果你不想在自定义Application中写这些便于测试的get方法,可以单独写出来,使用Robolectric@Config来单独指定自定义Application。

LoginDaggerActivity基本没有变化,只是不用初始化LoginDaggerPresenter了,可以直接使用了。因为Dagger已经帮我们自动创建好了。

public class LoginDaggerActivity extends BaseMVPDaggerActivity<LoginMvpView, LoginDaggerPresenter> implements LoginMvpView, View.OnClickListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        mPresenter.getIdentify(); //<--直接调用
        ....
    }
    ....其余不变
}

LoginDaggerPresenter变化也不大。只需要构造方法注入,Dagger就会自动帮我们注入我们需要的GithubApi。实际中,对于SP、数据库的操作都可以这样去写。

@ActivityScope
public class LoginDaggerPresenter extends BaseMVPPresenter<LoginMvpView> {

    private GithubApi mApi;

    @Inject
    public LoginDaggerPresenter(GithubApi mApi){
        this.mApi = mApi;
    }

   ....其余不变
}

最后AppComponent中统一注入各个Module。


@Module
public abstract class BuildersModule {

    @ActivityScope
    @ContributesAndroidInjector
    abstract LoginDaggerActivity loginDaggerActivityInjector();

}
@Singleton
@Component(modules = {
        AppModule.class,
        ClientModule.class,
        BuildersModule.class,
        AndroidSupportInjectionModule.class})
public interface AppComponent extends AndroidInjector<MyApp> {

    @Component.Builder
    abstract class Builder extends AndroidInjector.Builder<MyApp> {}

}

到这里,实现的代码基本就完了。下面我们来测试它。

2.单元测试代码

Activity的部分不变,有点小变动的主要是Presenter的测试部分。

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class LoginDaggerPresenterTest {

    private LoginDaggerPresenter mPresenter;

    @Mock
    private LoginMvpView mvpView;

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Before
    public void setUp(){
        ShadowLog.stream = System.out;
        //<---实例化Presenter
        mPresenter = new LoginDaggerPresenter(MyApp.getInstance().getGithubApi());
        mPresenter.attachView(mvpView);
    }

   ....其余不变
}

可以看出来加入了Dagger后的变化并不是很大,测试起来还是很方便的。

3.使用Jacoco

之前有人在github上问我,有没有使用jacoco生成测试报告。其实使用起来并不是很麻烦。

新建一个jacoco.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/zl/weilu/androidut", //指定类文件夹
            includes: ["**/*Presenter.*"], //包含类的规则,这里我们生成所有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") //指定报告数据的路径
}

然后在应用Module下的build.gradle文件中引用刚新建的gradle文件:

apply from: 'jacoco.gradle'

sync项目之后可以在任务列表看到新增加的任务:
这里写图片描述

其中testDebugUnitTest任务会生成单元测试结果报告,包含xml及html格式分别对应test-resultsreports文件夹;jacocoTestReport任务会生成单元测试覆盖率报告,结果存放在jacocoJacocoReport文件夹。以上的文件都在对应Module的build文件下。

这里写图片描述

本项目的测试报告如下:

这里写图片描述

代码覆盖率如下:

这里写图片描述

详细的内容我们可以在生成的html页面中点击查看。

这里写图片描述

PS:如果有乱码问题,可以使用gradlew -Dfile.encoding=UTF-8 jacocoTestReport来解决

使用前:

这里写图片描述

使用后:

这里写图片描述

红色表示未测试部分,绿色表示测试部分,还有一种黄色表示部分测试,比如if else的部分分支被执行。

4.参考

本篇的内容不是很多,但是涵盖的东西却是之前的总和。掌握了这些,一般场景下的单元测试你就已经得心应手了。到这里,单元测试的内容基本就结束了。后面我会查漏补缺一下,看有没有遗漏的地方需要补充。就这样了。。。。

所有代码已上传至Github。希望大家多多点赞支持!