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
的工作原理:
- 初始化一個測試類裡面的所有用
@Mock
field為mock物件(loginPresenter
) - mock
AppModule
,通過反射的方式得到AppModule
的所有provider方法,如果有某個方法的返回值是一個LoginPresenter
,那麼就使用Mockito,讓這個方法(provideLoginPresenter(...)
)被呼叫時,返回我們在測試類裡面定義的mockloginPresenter
。 - 使用這個mock AppModule來構建一個Component,並且放到
ComponentHolder
裡面去。
我相信看到這裡,你一定有很多疑問:
- 它怎麼知道要使用
AppModule
- 它怎麼知道要build什麼樣的Componant
- 它怎麼知道要把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。
獲取最新文章或想加入安卓單元測試交流群,請關注下方公眾號