17

由於DI的概念完全有意義,因此我最近用Dagger全身心地投入了工作。 DI的更好的「副產品」之一(就像傑克沃頓在他的演講中所說的那樣)是易於測試的。讓Dagger在爲Android做咖啡功能測試時注入模擬物體

所以現在我基本上使用espresso來做一些功能測試,並且我希望能夠嚮應用程序注入虛擬/模擬數據並讓活動顯示出來。我猜想,這是DI的最大優勢之一,這應該是一個相對簡單的問題。不過,出於某種原因,我似乎無法把頭圍住它。任何幫助將非常感激。這是我迄今(我已經寫了反映我的當前設置爲例):

public class MyActivity 
    extends MyBaseActivity { 

    @Inject Navigator _navigator; 

    @Override 
    public void onCreate(Bundle savedInstanceState) { 
     super.onCreate(savedInstanceState); 
     MyApplication.get(this).inject(this); 

     // ... 

     setupViews(); 
    } 

    private void setupViews() { 
     myTextView.setText(getMyLabel()); 
    } 

    public String getMyLabel() { 
     return _navigator.getSpecialText(); // "Special Text" 
    } 
} 

這些都是我的匕首模塊:

// Navigation Module 

@Module(library = true) 
public class NavigationModule { 

    private Navigator _nav; 

    @Provides 
    @Singleton 
    Navigator provideANavigator() { 
     if (_nav == null) { 
      _nav = new Navigator(); 
     } 
     return _nav; 
    } 
} 

// App level module 

@Module(
    includes = { SessionModule.class, NavigationModule.class }, 
    injects = { MyApplication.class, 
       MyActivity.class, 
       // ... 
}) 
public class App { 
    private final Context _appContext; 
    AppModule(Context appContext) { 
     _appContext = appContext; 
    } 
    // ... 
} 

在我的咖啡測試,我想插入模擬模塊,如下所示:

public class MyActivityTest 
    extends ActivityInstrumentationTestCase2<MyActivity> { 

    public MyActivityTest() { 
     super(MyActivity.class); 
    } 

    @Override 
    public void setUp() throws Exception { 
     super.setUp(); 
     ObjectGraph og = ((MyApplication) getActivity().getApplication()).getObjectGraph().plus(new TestNavigationModule()); 
     og.inject(getActivity()); 
    } 

    public void test_SeeSpecialText() { 
     onView(withId(R.id.my_text_view)).check(matches(withText(
      "Special Dummy Text))); 
    } 

    @Module(includes = NavigationModule.class, 
      injects = { MyActivityTest.class, MyActivity.class }, 
      overrides = true, 
      library = true) 
    static class TestNavigationModule { 

     @Provides 
     @Singleton 
     Navigator provideANavigator() { 
      return new DummyNavigator(); // that returns "Special Dummy Text" 
     } 
    } 
} 

這根本不起作用。我的咖啡測試運行,但TestNavigationModule完全被忽略... arr ... :(

我在做什麼錯了嗎?有沒有更好的方法來嘲笑模塊與咖啡出來我已經搜索和看到的例子Robolectric,使用等的Mockito但我只想純咖啡的測試和需要更換一個模塊與我的模擬一個我應該怎麼做這個

編輯:。。?

所以我決定用@ user3399328方法有一個靜態測試模塊列表定義,檢查null,然後將它添加到我的應用程序類。我仍然沒有得到我的測試注入版本的類,雖然我有一種感覺,但它可能是錯誤的用匕首測試模塊定義,而不是我的espresso生命週期。我做這個假設的原因是我添加了調試語句,並發現在應用程序類中注入靜態測試模塊時非空。你能否指出我可能做錯的方向?下面是我定義的代碼片段:

所有MyApplication:

@Override 
public void onCreate() { 
    // ... 
    mObjectGraph = ObjectGraph.create(Modules.list(this)); 
    // ... 
} 

模塊:

public class Modules { 

    public static List<Object> _testModules = null; 

    public static Object[] list(MyApplication app) { 
     //  return new Object[]{ new AppModule(app) }; 
     List<Object> modules = new ArrayList<Object>(); 
     modules.add(new AppModule(app)); 

     if (_testModules == null) { 
      Log.d("No test modules"); 
     } else { 
      Log.d("Test modules found"); 
     } 

     if (_testModules != null) { 
      modules.addAll(_testModules); 
     } 

     return modules.toArray(); 
    } 
} 

修改了我的測試類中的測試模塊:

@Module(overrides = true, library = true) 
public static class TestNavigationModule { 

    @Provides 
    @Singleton 
    Navigator provideANavigator()() { 
     Navigator navigator = new Navigator(); 
     navigator.setSpecialText("Dummy Text"); 
     return navigator; 
    } 
} 

回答

8

你的方法不起作用,因爲它只發生一次,正如馬特所說,當活動的真實注入代碼運行時,它將消除由特殊對象圖注入的任何變量。

有兩種方法可以使其工作。

的快捷方式:在您的活動一個公共靜態變量,這樣的測試可以指定一個重寫模塊,並有實際的活動代碼總是包含這個模塊,如果它不是空(只將在測試中發生)。這與我的回答here類似,只是針對您的活動基類而不是應用程序。

的時間越長,可能是更好的方法:重構你的代碼,使所有活動注入(更重要的圖表生成)發生在一類,像ActivityInjectHelper。在您的測試包,創建一個名爲ActivityInjectHelper與實現相同的方法完全相同的包路徑另一個類,但也加分測試模塊。由於測試類首先被加載,您的應用程序將與測試ActivityInjectHelper一起執行。再次,它類似於我的回答here只是針對不同的課程。

UPDATE:

我看你已經發布更多的代碼和它接近的工作,但沒有雪茄。對於活動和應用程序,測試模塊需要在onCreate()運行之前進行緩衝。在處理活動對象圖時,在測試的getActivity()之前的任何時候都可以。當應用程序處理,這是一個有點難度,因爲的onCreate()已經在一次設置()調用運行。幸運的是,在測試的構造函數中執行 - 應用程序尚未在此時創建。我在第一個鏈接中簡要提到了這一點。

+1

這是要走的路。巧妙地繞過這個限制。先生們搖滾吧! –

+0

當我嘗試在我的ActivityInstrumentationTestCase2子類的構造函數中注入我的測試模塊時,它不起作用,因爲應用程序已經實例化並且onCreate已被調用。所以我仍然沒有找到運行測試時聲明我的測試模塊的好方法 –

+0

我目前正在嘗試@ user3399328的第二個解決方案(在我的測試源中提供另一版本的ActivityInjectHelper)。你有沒有找到一種方法讓它與Gradle一起工作?我一直在運行Android Studio中彈出的文件[文件名]錯誤中找到的重複類。 – Ben

1

到getActivity呼叫將實際上開始你的活動調用onCreate在這個過程中,這意味着你不會得到你的tes t模塊添加到圖表中以便及時使用。使用activityInstrumentationTestcase2,您無法在活動範圍內正確注入。我已經通過使用我的應用程序爲我的活動提供依賴關係來解決此問題,然後將活動將使用的模擬對象注入到其中。這不是理想的,但它的工作原理。您可以使用像Otto這樣的事件總線來幫助提供依賴關係。

0

編輯:下面的帖子形式http://systemdotrun.blogspot.co.uk/2014/11/android-testing-with-dagger-retrofit.html

要使用咖啡+匕首我已經做了來自@回答以下

啓發user3399328我有我的應用類中的​​類測試一個Activity,這允許測試情況下,使用測試@Modules其供給嘲笑重寫@Provider秒。只要

1)這是getActivity()呼叫由與測試用例之前進行(如我的注入呼叫在我的活動發生內部Activity.onCreate

2)拆卸將刪除對象圖中的測試模塊。

下面的例子。

注意:這並不理想,因爲這受到使用IoC工廠方法的類似缺陷的影響,但至少這樣它在tearDown()中唯一的一次調用將被測系統恢復正常。

Application類中的​​

public static class DaggerHelper 
{ 
    private static ObjectGraph sObjectGraph; 

    private static final List<Object> productionModules; 

    static 
    { 
     productionModules = new ArrayList<Object>(); 
     productionModules.add(new DefaultModule()); 
    } 

    /** 
    * Init the dagger object graph with production modules 
    */ 
    public static void initProductionModules() 
    { 
     initWithModules(productionModules); 
    } 

    /** 
    * If passing in test modules make sure to override = true in the @Module annotation 
    */ 
    public static void initWithTestModules(Object... testModules) 
    { 
     initWithModules(getModulesAsList(testModules)); 
    } 

    private static void initWithModules(List<Object> modules) 
    { 
     sObjectGraph = ObjectGraph.create(modules.toArray()); 
    } 

    private static List<Object> getModulesAsList(Object... extraModules) 
    { 
     List<Object> allModules = new ArrayList<Object>(); 
     allModules.addAll(productionModules); 
     allModules.addAll(Arrays.asList(extraModules)); 
     return allModules; 
    } 

    /** 
    * Dagger convenience method - will inject the fields of the passed in object 
    */ 
    public static void inject(Object object) { 
     sObjectGraph.inject(object); 
    } 
} 

我的測試類中我的測試模塊

@Module (
     overrides = true, 
     injects = ActivityUnderTest.class 
) 
static class TestDataPersisterModule { 
    @Provides 
    @Singleton 
    DataPersister provideMockDataPersister() { 
     return new DataPersister(){ 
      @Override 
      public void persistDose() 
      { 
       throw new RuntimeException("Mock DI!"); //just a test to see if being called 
      } 
     }; 
    } 
} 

試驗方法

public void testSomething() 
{ 
    MyApp.DaggerHelper.initWithTestModules(new TestDataPersisterModule()); 
    getActivity(); 
    ... 
} 

推倒

@Override 
public void tearDown() throws Exception 
{ 
    super.tearDown(); 
    //reset 
    MyApp.DaggerHelper.initProductionModules(); 
} 
8

匕首2和咖啡兩件事情確實有所改善。這就是測試案例現在的樣子。請注意,ContributorsModel由Dagger提供。完整的演示在這裏可用:https://github.com/pmellaaho/RxApp

@RunWith(AndroidJUnit4.class) 
public class MainActivityTest { 

ContributorsModel mModel; 

@Singleton 
@Component(modules = MockNetworkModule.class) 
public interface MockNetworkComponent extends RxApp.NetworkComponent { 
} 

@Rule 
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
     MainActivity.class, 
     true,  // initialTouchMode 
     false); // launchActivity. 

@Before 
public void setUp() { 
    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 
    RxApp app = (RxApp) instrumentation.getTargetContext() 
      .getApplicationContext(); 

    MockNetworkComponent testComponent = DaggerMainActivityTest_MockNetworkComponent.builder() 
      .mockNetworkModule(new MockNetworkModule()) 
      .build(); 
    app.setComponent(testComponent); 
    mModel = testComponent.contributorsModel(); 
} 

@Test 
public void listWithTwoContributors() { 

    // GIVEN 
    List<Contributor> tmpList = new ArrayList<>(); 
    tmpList.add(new Contributor("Jesse", 600)); 
    tmpList.add(new Contributor("Jake", 200)); 

    Observable<List<Contributor>> testObservable = Observable.just(tmpList); 

    Mockito.when(mModel.getContributors(anyString(), anyString())) 
      .thenReturn(testObservable); 

    // WHEN 
    mActivityRule.launchActivity(new Intent()); 
    onView(withId(R.id.startBtn)).perform(click()); 

    // THEN 
    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0)) 
      .check(matches(hasDescendant(withText("Jesse")))); 

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0)) 
      .check(matches(hasDescendant(withText("600")))); 

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1)) 
      .check(matches(hasDescendant(withText("Jake")))); 

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1)) 
      .check(matches(hasDescendant(withText("200")))); 
} 
+2

這也是我找到的最好的方式。 1)在您的應用程序上展示DI容器2)讓ActivityTestRule不會自動啓動您的應用程序3)在測試方法(或設置)中更改DI容器4)手動啓動您的應用程序5)測試。 – newfivefour