安卓單元測試 (十):DaggerMock, 讓 Dagger2 與單元測試的結合易如反掌

小創發表於2017-03-24

The Old Way

我們在系列的第六篇文章前面介紹了Dagger2在單元測試裡面的使用姿勢。大致過程是這樣的,首先,你要mock出一個Module,讓它的某個Provider方法在被呼叫的時候,返回你想到的mock的Dependency。然後使用這個mock的module來build出一個Component,再把這個Component放到你的ComponentHolder。舉個例子說明一下,假設你有一個LoginActivity,裡面有一個LoginPresenter,是通過Dagger2 inject進去的,如下:

public class LoginActivity extends AppCompatActivity {
    @Inject
    LoginPresenter mLoginPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...other code

        ComponentHolder.getAppComponent().inject(this);
    }
}

//對應的Test類如下:

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class LoginActivityTest {
    @Test
    public void testLogin() {
        AppModule mockAppModule = Mockito.mock(AppModule.class);
        LoginPresenter mockLoginPresenter = mock(LoginPresenter.class);
        Mockito.when(mockAppModule.provideLoginPresenter(any(UserManager.class), any(PasswordValidator.class))).thenReturn(mockLoginPresenter);  //當mockAppModule的provideLoginPresenter()方法被呼叫時,讓它返回mockLoginPresenter
        AppComponent appComponent = DaggerAppComponent.builder().appModule(mockAppModule).build();  //用mockAppModule來建立DaggerAppComponent
        ComponentHolder.setAppComponent(appComponent);  //假設你的Component是放在ComponentHolder裡面的

        LoginActivity loginActivity = Robolectric.setupActivity(LoginActivity.class);
        ((EditText) loginActivity.findViewById(R.id.username)).setText("xiaochuang");
        ((EditText) loginActivity.findViewById(R.id.password)).setText("xiaochuang is handsome");
        loginActivity.findViewById(R.id.login).performClick();    

        verify(mockLoginPresenter).login("xiaochuang", "xiaochuang is handsome");
    }
}複製程式碼

可以看到,為了讓Dagger2返回一個Mock物件,我們需要寫5行程式碼。再多寫幾個測試,我保證你一定會覺得繁瑣的。當然,我們可以使用前一篇文章裡面說的方式,和其它的一些手段,來簡化程式碼,以下是我作出的一些努力,應該說,程式碼已經比較簡潔了:

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class LoginActivityTest {

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

    @Mock
    LoginPresenter loginPresenter;

    @Test
    public void testLogin() {
        Mockito.when(TestUtils.appModule.provideLoginPresenter(any(UserManager.class), any(PasswordValidator.class))).thenReturn(loginPresenter);  //當mockAppModule的provideLoginPresenter()方法被呼叫時,讓它返回mockLoginPresenter
        TestUtils.setupDagger();

        LoginActivity loginActivity = Robolectric.setupActivity(LoginActivity.class);
        ((EditText) loginActivity.findViewById(R.id.username)).setText("xiaochuang");
        ((EditText) loginActivity.findViewById(R.id.password)).setText("xiaochuang is handsome");
        loginActivity.findViewById(R.id.login).performClick();

        verify(loginPresenter).login("xiaochuang", "xiaochuang is handsome");
    }

}

public class TestUtils {
    public static final AppModule appModule = spy(new AppModule(RuntimeEnvironment.application));
    public static void setupDagger() {
        AppComponent appComponent = DaggerAppComponent.builder().appModule(appModule).build();
        ComponentHolder.setAppComponent(appComponent);
    }
}複製程式碼

上面把dagger設定相關的程式碼減少到了兩行,應該說,已經不再是一個負擔了。然而哪怕是這樣,如果寫多了的話,依然會讓人感覺略煩,因為這也完全是Boilerplate code(這裡為什麼要用“也”?)。再多寫一點,你就會自然而然的想,如果能有一個工具,能達到這樣的效果就好了:我們在Test類裡面定義一個@Mock field(比如上面的loginPresenter),這個工具就能自動把這個field作為dagger的module對應的provider方法(provideLoginPresenter(...))的返回值。也就是說,自動的mock module,讓它返回這個@Mock field,然後用這個mock的module來build一個component,並放到ComponentHolder裡面去。

New Hope

Well,我寫這篇文章,就是想告訴大家,還真有人寫了這樣的一個工具,這就是這篇文章要介紹的DaggerMock。它就能達到我們上面描述的那種效果,讓我們像使用Mockito Annotation一樣來定義Mock,卻能自動把它們作為Dagger2生產的Dependency。達到的效果如下:

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class LoginActivityTest {

    @Rule public DaggerRule daggerRule = new DaggerRule();

    @Mock
    LoginPresenter loginPresenter;

    @Test
    public void testLogin_shinny_way() {
        LoginActivity loginActivity = Robolectric.setupActivity(LoginActivity.class);
        ((EditText) loginActivity.findViewById(R.id.username)).setText("xiaochuang");
        ((EditText) loginActivity.findViewById(R.id.password)).setText("xiaochuang is handsome");
        loginActivity.findViewById(R.id.login).performClick();

        verify(loginPresenter).login("xiaochuang", "xiaochuang is handsome");
    }
}複製程式碼

在上面的程式碼中,已經沒有多餘的Boilerplate code,要寫的程式碼,基本是必需寫的了。上面起作用的是@Rule public DaggerRule daggerRule = new DaggerRule(); 這行程式碼。可見,它是通過JUnit Rule來實現的。如果你熟悉JUnit Rule的工作原理,那麼你很容易猜到這個DaggerRule的工作原理:

  1. 初始化一個測試類裡面的所有用@Mock field為mock物件(loginPresenter)
  2. mock AppModule,通過反射的方式得到AppModule的所有provider方法,如果有某個方法的返回值是一個LoginPresenter,那麼就使用Mockito,讓這個方法(provideLoginPresenter(...))被呼叫時,返回我們在測試類裡面定義的mock loginPresenter
  3. 使用這個mock AppModule來構建一個Component,並且放到ComponentHolder裡面去。

我相信看到這裡,你一定有很多疑問:

  1. 它怎麼知道要使用AppModule
  2. 它怎麼知道要build什麼樣的Componant
  3. 它怎麼知道要把build出來的Component放到哪?

好吧,其實上面的DaggerRule,不是DaggerMock這個library自帶的,是我們自己實現的。然而彆著急,DaggerMock給了我們提供了一個父類Rule:DaggerMockRule,這個Rule已經幫我們做了絕大多數事情了。我們自定義的DaggerRule,其實也是繼承自DaggerMockRule的,而我們在自定義Rule裡面做的事情,也只不過是告訴DaggerMock,上面說到的三個問題的答案:要使用哪個Module、要build哪個Component、要把build好的Component放到哪,僅此而已。不信請看程式碼:

public class DaggerRule extends DaggerMockRule<AppComponent> {
    public DaggerRule() {
        //告訴DaggerMock要build什麼樣的Component,使用哪個module
        super(AppComponent.class, new AppModule(RuntimeEnvironment.application));

        //告訴DaggerMock把build好的Component放到哪
        set(new ComponentSetter<AppComponent>() {
            @Override
            public void setComponent(AppComponent appComponent) {
                ComponentHolder.setAppComponent(appComponent);
            }
        });
    }
}複製程式碼

怎麼樣,很簡單吧?這個DaggerRule是可以重複使用的,一般來說,一個Component類對應於一個這樣的DaggerRule就好了。自此,你可以只負責使用@Mock來定義mock了,dagger的事情就交給這個DaggerRule就好了。
是不是很爽!

哦對了,將這個library加到專案裡面的姿勢說一下,在build.gradle檔案裡面加入:

repositories {
    jcenter()
    maven { url "https://jitpack.io" }
}複製程式碼

dependencies {
    //...others dependencies

    testCompile 'com.github.fabioCollini:DaggerMock:0.6.1'
    androidTestCompile 'com.github.fabioCollini:DaggerMock:0.6.1' //如果你需要在Instrumentation、Espresso、UiAutomator裡面使用的話
}複製程式碼

我剛開始使用這個lib的時候,還是花了點時間來理解的,個人認為作者的README和對應的文章寫得都不算是很容易看懂,希望這篇文章能讓幫助到各位一點點。

照例文中的程式碼在github的這個repo

獲取最新文章或想加入安卓單元測試交流群,請關注下方公眾號

安卓單元測試 (十):DaggerMock, 讓 Dagger2 與單元測試的結合易如反掌

相關文章