Android單元測試(八):Dagger與單元測試

本篇緊接着上一篇的內容,對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。希望大家多多點贊支持!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章