【问题标题】:Getting Dagger to inject mock objects when doing Espresso functional testing for Android在为 Android 进行 Espresso 功能测试时让 Dagger 注入模拟对象
【发布时间】:2014-06-07 15:22:34
【问题描述】:

我最近完全使用 Dagger,因为 DI 的概念完全有意义。 DI 的一个更好的“副产品”(正如 Jake Wharton 在他的一次演讲中所说)是更容易测试。

所以现在我基本上使用 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;
    }
    // ...
}

在我的 Espresso 测试中,我试图插入一个模拟模块,如下所示:

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"
        }
    }
}

这根本不起作用。我的 Espresso 测试运行了,但 TestNavigationModule 完全被忽略了... arr... :(

我做错了什么?有没有更好的方法来用 Espresso 模拟模块?我已经搜索并看到了使用 Robolectric、Mockito 等的示例。但我只想要纯 Espresso 测试,并且需要用我的模拟模块换掉一个模块。我该怎么做?

编辑:

所以我采用@user3399328 的方法来定义静态测试模块列表,检查是否为空,然后将其添加到我的应用程序类中。我仍然没有得到我的测试注入版本的课程。不过我有一种感觉,可能是匕首测试模块定义有问题,而不是我的浓缩咖啡生命周期。我做出假设的原因是我添加了调试语句并发现静态测试模块在应用程序类中注入时是非空的。您能否指出我可能做错的方向。以下是我定义的代码 sn-ps:

我的应用程序:

@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;
    }
}

【问题讨论】:

    标签: android dependency-injection functional-testing dagger android-espresso


    【解决方案1】:

    有了 Dagger 2 和 Espresso 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"))));
    }
    

    【讨论】:

    • 这也是我找到的最好方法。 1)在您的应用程序上公开 DI 容器 2)使 ActivityTestRule 不会自动启动您的应用程序 3)在测试方法(或设置)中更改 DI 容器 4)手动启动您的应用程序 5)测试。
    【解决方案2】:

    你的方法行不通,因为它只发生一次,正如马特所说,当活动的真正注入代码运行时,它会清除你的特殊对象图注入的所有变量。

    有两种方法可以让它工作。

    快速方法:在您的活动中创建一个公共静态变量,以便测试可以分配一个覆盖模块并让实际活动代码始终包含该模块,如果它不为空(只会在测试中发生)。它类似于我的回答 here 只是针对您的活动基类而不是应用程序。

    更长,可能更好的方法:重构您的代码,以便所有活动注入(更重要的是图形创建)都发生在一个类中,例如 ActivityInjectHelper。在您的测试包中,创建另一个名为 ActivityInjectHelper 的类,该类具有实现相同方法的完全相同包路径,除了还加上您的测试模块。因为首先加载了测试类,所以您的应用程序将使用测试 ActivityInjectHelper 执行。同样,它与我的答案 here 相似,只是针对不同的班级。

    更新:

    我看到你发布了更多代码,它接近工作,但没有雪茄。对于活动和应用程序,测试模块需要在 onCreate() 运行之前潜入。在处理活动对象图时,在测试的 getActivity() 之前的任何时间都可以。在处理应用程序时,这有点困难,因为在 setUp() 运行时已经调用了 onCreate()。幸运的是,在测试的构造函数中这样做是可行的——那时还没有创建应用程序。我在第一个链接中简要提到了这一点。

    【讨论】:

    • 这是要走的路。绕过限制的巧妙方法。摇滚先生!
    • 当我尝试在我的 ActivityInstrumentationTestCase2 子类的构造函数中注入我的测试模块时,它不起作用,因为 Application 已经被实例化并且 onCreate 已经被调用。所以我仍然没有找到在运行测试时声明我的测试模块的好方法
    • 我目前正在尝试@user3399328 的第二个解决方案(在我的测试源中提供另一个版本的 ActivityInjectHelper)。你有没有找到一种方法让它与 Gradle 一起工作?我一直遇到在 Android Studio 中弹出的Duplicate class found in the file [filename] 错误。
    【解决方案3】:

    对 getActivity 的调用实际上会启动您在流程中调用 onCreate 的活动,这意味着您不会及时将测试模块添加到图表中以供使用。使用 activityInstrumentationTestcase2 您不能真正在活动范围内正确注入。我通过使用我的应用程序为我的活动提供依赖项,然后将模拟对象注入其中,活动将使用它来解决这个问题。这并不理想,但它有效。您可以使用 Otto 之类的事件总线来帮助提供依赖项。

    【讨论】:

      【解决方案4】:

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

      要使用 Espresso + Dagger 测试 Activity,我已完成以下操作

      受@user3399328 的回答启发,我的Application 类中有一个DaggerHelper 类,它允许测试用例使用提供模拟的Test @Modules 覆盖@Providers。只要

      1) 这是在调用 testCases getActivity() 之前完成的(因为我的注入调用发生在Activity.onCreate 内部的活动中)

      2) tearDown 从对象图中删除测试模块。

      以下示例。

      注意:这并不理想,因为在使用 IoC 工厂方法时会遇到类似的陷阱,但至少在这种情况下,它只需要一次调用 tearDown() 即可使被测系统恢复正常。

      DaggerHelper 在我的 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();
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-01-29
        • 1970-01-01
        • 1970-01-01
        • 2018-07-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多