65

I have a simple Android activity with a single dependency. I inject the dependency into the activity's onCreate like this:

Dagger_HelloComponent.builder()
    .helloModule(new HelloModule(this))
    .build()
    .initialize(this);

In my ActivityUnitTestCase I want to override the dependency with a Mockito mock. I assume I need to use a test-specific module which provides the mock, but I can't figure out how to add this module to the object graph.

In Dagger 1.x this is apparently done with something like this:

@Before
public void setUp() {
  ObjectGraph.create(new TestModule()).inject(this);
}

What's the Dagger 2.0 equivalent of the above?

You can see my project and its unit test here on GitHub.

4

9 回答 9

50

可能这更像是一种适当支持测试模块覆盖的解决方法,但它允许使用测试模块覆盖生产模块。下面的代码片段显示了当您只有一个组件和一个模块时的简单情况,但这应该适用于任何情况。它需要大量的样板和代码重复,因此请注意这一点。我相信将来会有更好的方法来实现这一目标。

我还创建了一个包含 Espresso 和 Robolectric 示例的项目。此答案基于项目中包含的代码。

解决方案需要两件事:

  • 提供额外的二传手@Component
  • 测试组件必须扩展生产组件

假设我们很简单Application,如下所示:

public class App extends Application {

    private AppComponent mAppComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        mAppComponent = DaggerApp_AppComponent.create();
    }

    public AppComponent component() {
        return mAppComponent;
    }

    @Singleton
    @Component(modules = StringHolderModule.class)
    public interface AppComponent {

        void inject(MainActivity activity);
    }

    @Module
    public static class StringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder("Release string");
        }
    }
}

我们必须在App类中添加额外的方法。这使我们能够更换生产组件。

/**
 * Visible only for testing purposes.
 */
// @VisibleForTesting
public void setTestComponent(AppComponent appComponent) {
    mAppComponent = appComponent;
}

如您所见,该StringHolder对象包含“发布字符串”值。这个对象被注入到MainActivity.

public class MainActivity extends ActionBarActivity {

    @Inject
    StringHolder mStringHolder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ((App) getApplication()).component().inject(this);
    }
}

在我们的测试中,我们希望提供StringHolder“测试字符串”。我们必须在创建App之前在类中设置测试组件MainActivity- 因为StringHolderonCreate回调中注入。

在 Dagger v2.0.0 中,组件可以扩展其他接口。我们可以利用它来创建我们的TestAppComponentwhich extends AppComponent

@Component(modules = TestStringHolderModule.class)
interface TestAppComponent extends AppComponent {

}

现在我们可以定义我们的测试模块,例如TestStringHolderModule. 最后一步是使用之前在App类中添加的 setter 方法设置测试组件。在创建活动之前执行此操作很重要。

((App) application).setTestComponent(mTestAppComponent);

浓咖啡

对于 Espresso,我创建了自定义ActivityTestRule,允许在创建活动之前交换组件。DaggerActivityTestRule 你可以在这里找到代码。

用 Espresso 进行样品测试:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityEspressoTest {

    public static final String TEST_STRING = "Test string";

    private TestAppComponent mTestAppComponent;

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule =
            new DaggerActivityTestRule<>(MainActivity.class, new OnBeforeActivityLaunchedListener<MainActivity>() {
                @Override
                public void beforeActivityLaunched(@NonNull Application application, @NonNull MainActivity activity) {
                    mTestAppComponent = DaggerMainActivityEspressoTest_TestAppComponent.create();
                    ((App) application).setTestComponent(mTestAppComponent);
                }
            });

    @Component(modules = TestStringHolderModule.class)
    interface TestAppComponent extends AppComponent {

    }

    @Module
    static class TestStringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder(TEST_STRING);
        }
    }

    @Test
    public void checkSomething() {
        // given
        ...

        // when
        onView(...)

        // then
        onView(...)
                .check(...);
    }
}

机器人电动

多亏了 .Robolectric,使用它更容易RuntimeEnvironment.application

使用 Robolectric 进行样本测试:

@RunWith(RobolectricGradleTestRunner.class)
@Config(emulateSdk = 21, reportSdk = 21, constants = BuildConfig.class)
public class MainActivityRobolectricTest {

    public static final String TEST_STRING = "Test string";

    @Before
    public void setTestComponent() {
        AppComponent appComponent = DaggerMainActivityRobolectricTest_TestAppComponent.create();
        ((App) RuntimeEnvironment.application).setTestComponent(appComponent);
    }

    @Component(modules = TestStringHolderModule.class)
    interface TestAppComponent extends AppComponent {

    }

    @Module
    static class TestStringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder(TEST_STRING);
        }
    }

    @Test
    public void checkSomething() {
        // given
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

        // when
        ...

        // then
        assertThat(...)
    }
}
于 2015-05-01T23:03:42.743 回答
25

正如@EpicPandaForce 所说,您不能扩展模块。然而,我想出了一个偷偷摸摸的解决方法,我认为它避免了其他示例所遭受的许多样板。

“扩展”模块的技巧是创建一个部分模拟,并模拟出您想要覆盖的提供者方法。

使用模拟

MyModule module = Mockito.spy(new MyModule());
Mockito.doReturn("mocked string").when(module).provideString();

MyComponent component = DaggerMyComponent.builder()
        .myModule(module)
        .build();

app.setComponent(component);

我在这里创建了这个要点来展示一个完整的例子。

编辑

事实证明,即使没有部分模拟,您也可以这样做,如下所示:

MyComponent component = DaggerMyComponent.builder()
        .myModule(new MyModule() {
            @Override public String provideString() {
                return "mocked string";
            }
        })
        .build();

app.setComponent(component);
于 2015-08-27T15:12:51.983 回答
11

@tomrozb 提出的解决方法非常好,让我走上了正轨,但我的问题是它setTestComponent()在 PRODUCTIONApplication类中暴露了一个方法。我能够让这个工作略有不同,这样我的生产应用程序就不必知道任何关于我的测试环境的事情。

TL;DR - 通过使用您的测试组件和模块的测试应用程序扩展您的应用程序类。然后创建一个在测试应用程序而不是生产应用程序上运行的自定义测试运行程序。


编辑:此方法仅适用于全局依赖项(通常标有@Singleton)。如果您的应用程序具有不同范围的组件(例如每个活动),那么您需要为每个范围创建子类,或者使用@tomrozb 的原始答案。感谢@tomrozb 指出这一点!


此示例使用AndroidJUnitRunner测试运行程序,但这可能适用于Robolectric等。

首先,我的生产应用程序。它看起来像这样:

public class MyApp extends Application {
    protected MyComponent component;

    public void setComponent() {
        component = DaggerMyComponent.builder()
                .myModule(new MyModule())
                .build();
        component.inject(this);
    }

    public MyComponent getComponent() {
        return component;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        setComponent();
    }
}

这样,我的活动和其他使用的类@Inject只需调用类似getApp().getComponent().inject(this);将自己注入依赖关系图中的东西。

为了完整起见,这是我的组件:

@Singleton
@Component(modules = {MyModule.class})
public interface MyComponent {
    void inject(MyApp app);
    // other injects and getters
}

我的模块:

@Module
public class MyModule {
    // EDIT: This solution only works for global dependencies
    @Provides @Singleton
    public MyClass provideMyClass() { ... }

    // ... other providers
}

对于测试环境,从生产组件扩展您的测试组件。这与@tomrozb 的答案相同。

@Singleton
@Component(modules = {MyTestModule.class})
public interface MyTestComponent extends MyComponent {
    // more component methods if necessary
}

测试模块可以是任何你想要的。大概你会在这里处理你的嘲笑和东西(我使用 Mockito)。

@Module
public class MyTestModule {
    // EDIT: This solution only works for global dependencies
    @Provides @Singleton
    public MyClass provideMyClass() { ... }

    // Make sure to implement all the same methods here that are in MyModule, 
    // even though it's not an override.
}

所以现在,棘手的部分。创建一个从您的生产应用程序类扩展的测试应用程序类,并覆盖该setComponent()方法以使用测试模块设置测试组件。请注意,这只有在MyTestComponent是 的后代时才有效MyComponent

public class MyTestApp extends MyApp {

    // Make sure to call this method during setup of your tests!
    @Override
    public void setComponent() {
        component = DaggerMyTestComponent.builder()
                .myTestModule(new MyTestModule())
                .build();
        component.inject(this)
    }
}

确保在开始测试之前调用setComponent()应用程序,以确保图表设置正确。像这样的东西:

@Before
public void setUp() {
    MyTestApp app = (MyTestApp) getInstrumentation().getTargetContext().getApplicationContext();
    app.setComponent()
    ((MyTestComponent) app.getComponent()).inject(this)
}

最后,最后缺少的部分是使用自定义测试运行器覆盖您的 TestRunner。在我的项目中,我使用的是,AndroidJUnitRunner但看起来你可以用 Robolectric 做同样的事情

public class TestRunner extends AndroidJUnitRunner {
    @Override
    public Application newApplication(@NonNull ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        return super.newApplication(cl, MyTestApp.class.getName(), context);
    }
}

您还必须更新您的testInstrumentationRunnergradle,如下所示:

testInstrumentationRunner "com.mypackage.TestRunner"

如果您使用的是 Android Studio,您还必须从运行菜单中单击编辑配置,然后在“特定检测运行程序”下输入您的测试运行程序的名称。

就是这样!希望这些信息对某人有所帮助:)

于 2015-08-14T23:34:58.633 回答
2

看来我已经找到了另一种方法,并且到目前为止它正在工作。

首先,一个不是组件本身的组件接口:

我的组件.java

interface MyComponent {
    Foo provideFoo();
}

然后我们有两个不同的模块:实际模块和测试模块。

我的模块.java

@Module
class MyModule {
    @Provides
    public Foo getFoo() {
        return new Foo();
    }
}

测试模块.java

@Module
class TestModule {
    private Foo foo;
    public void setFoo(Foo foo) {
        this.foo = foo;
    }

    @Provides
    public Foo getFoo() {
        return foo;
    }
}

我们有两个组件来使用这两个模块:

MyRealComponent.java

@Component(modules=MyModule.class)
interface MyRealComponent extends MyComponent {
    Foo provideFoo(); // without this dagger will not do its magic
}

MyTestComponent.java

@Component(modules=TestModule.class)
interface MyTestComponent extends MyComponent {
    Foo provideFoo();
}

在应用程序中,我们这样做:

MyComponent component = DaggerMyRealComponent.create();
<...>
Foo foo = component.getFoo();

在测试代​​码中,我们使用:

TestModule testModule = new TestModule();
testModule.setFoo(someMockFoo);
MyComponent component = DaggerMyTestComponent.builder()
    .testModule(testModule).build();
<...>
Foo foo = component.getFoo(); // will return someMockFoo

问题是我们必须将 MyModule 的所有方法复制到 TestModule 中,但可以通过将 MyModule 放入 TestModule 并使用 MyModule 的方法来完成,除非它们是直接从外部设置的。像这样:

测试模块.java

@Module
class TestModule {
    MyModule myModule = new MyModule();
    private Foo foo = myModule.getFoo();
    public void setFoo(Foo foo) {
        this.foo = foo;
    }

    @Provides
    public Foo getFoo() {
        return foo;
    }
}
于 2016-02-02T17:22:30.943 回答
1

这个答案已经过时了。在编辑中阅读以下内容。

令人失望的是,您不能从 Module 扩展,否则您将收到以下编译错误:

Error:(24, 21) error: @Provides methods may not override another method.
Overrides: Provides 
    retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.mySe‌​rverEndpoint()

这意味着您不能仅扩展“模拟模块”并替换原始模块。不,没那么容易。考虑到你设计你的组件的方式是直接按类绑定模块,你也不能真的只做一个“TestComponent”,因为这意味着你必须从头开始重新发明一切,你会拥有为每个变化制作一个组件!显然,这不是一个选择。

所以在较小的规模上,我最终做的是制作一个我给模块的“提供者”,它决定了我是选择模拟还是生产类型。

public interface EndpointProvider {
    Endpoint serverEndpoint();
}

public class ProdEndpointProvider implements EndpointProvider {

    @Override
    public Endpoint serverEndpoint() {
        return new ServerEndpoint();
    }
}


public class TestEndpointProvider implements EndpointProvider {
    @Override
    public Endpoint serverEndpoint() {
        return new TestServerEndpoint();
    }
}

@Module
public class EndpointModule {
    private Endpoint serverEndpoint;

    private EndpointProvider endpointProvider;

    public EndpointModule(EndpointProvider endpointProvider) {
        this.endpointProvider = endpointProvider;
    }

    @Named("server")
    @Provides
    public Endpoint serverEndpoint() {
        return endpointProvider.serverEndpoint();
    }
}

编辑:显然,正如错误消息所说,您不能使用带@Provides注释的方法覆盖另一个方法,但这并不意味着您不能覆盖带@Provides注释的方法:(

所有的魔法都是徒劳的!您可以在不@Provides使用该方法的情况下扩展一个模块并且它可以工作......请参阅@vaughandroid 的答案。

于 2015-06-09T15:00:00.390 回答
0

你们可以看看我的解决方案吗,我已经包含了子组件示例:https ://github.com/nongdenchet/android-mvvm-with-tests 。谢谢@vaughandroid,我借用了你的压倒一切的方法。这是要点:

  1. 我创建一个类来创建子组件。我的自定义应用程序还将包含此类的一个实例:

    // The builder class
    public class ComponentBuilder {
     private AppComponent appComponent;
    
     public ComponentBuilder(AppComponent appComponent) {
      this.appComponent = appComponent;
     }
    
     public PlacesComponent placesComponent() {
      return appComponent.plus(new PlacesModule());
     }
    
     public PurchaseComponent purchaseComponent() {
      return appComponent.plus(new PurchaseModule());
     }
    }
    
    // My custom application class
    public class MyApplication extends Application {
    
     protected AppComponent mAppComponent;
     protected ComponentBuilder mComponentBuilder;
    
     @Override
     public void onCreate() {
      super.onCreate();
    
      // Create app component
      mAppComponent = DaggerAppComponent.builder()
              .appModule(new AppModule())
              .build();
    
      // Create component builder
      mComponentBuilder = new ComponentBuilder(mAppComponent);
     }
    
     public AppComponent component() {
      return mAppComponent;
     }
    
     public ComponentBuilder builder() {
      return mComponentBuilder;
     } 
    }
    
    // Sample using builder class:
    public class PurchaseActivity extends BaseActivity {
     ...    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
      ...
      // Setup dependency
      ((MyApplication) getApplication())
              .builder()
              .purchaseComponent()
              .inject(this);
      ...
     }
    }
    
  2. 我有一个自定义的 TestApplication 扩展了上面的 MyApplication 类。该类包含两个替换根组件和构建器的方法:

    public class TestApplication extends MyApplication {
     public void setComponent(AppComponent appComponent) {
      this.mAppComponent = appComponent;
     }
    
     public void setComponentBuilder(ComponentBuilder componentBuilder) {
      this.mComponentBuilder = componentBuilder;
     }
    }    
    
  3. 最后,我将尝试模拟或存根模块和构建器的依赖关系,以提供对活动的虚假依赖:

    @MediumTest
    @RunWith(AndroidJUnit4.class)
    public class PurchaseActivityTest {
    
     @Rule
     public ActivityTestRule<PurchaseActivity> activityTestRule =
         new ActivityTestRule<>(PurchaseActivity.class, true, false);
    
     @Before
     public void setUp() throws Exception {
     PurchaseModule stubModule = new PurchaseModule() {
         @Provides
         @ViewScope
         public IPurchaseViewModel providePurchaseViewModel(IPurchaseApi purchaseApi) {
             return new StubPurchaseViewModel();
         }
     };
    
     // Setup test component
     AppComponent component = ApplicationUtils.application().component();
     ApplicationUtils.application().setComponentBuilder(new ComponentBuilder(component) {
         @Override
         public PurchaseComponent purchaseComponent() {
             return component.plus(stubModule);
         }
     });
    
     // Run the activity
     activityTestRule.launchActivity(new Intent());
    }
    
于 2015-10-25T09:44:16.637 回答
0

对我来说,以下效果最好。

这不是一个测试友好的解决方案,但我经常使用它来模拟一些 API,同时在后端尚未准备好但我需要提前实现 UI 时进行开发。

Dagger (2.29.1) 不允许覆盖模块中提供的方法:

Binding methods may not be overridden in modules

但是,您可以使用一些额外的样板来欺骗它:

@Module
open class NetworkServicesModule {

    /**
     * Provide a mock instead
     */
    protected open fun doProvideFlightService(context: Context, retrofit: Retrofit): FlightService {
        return retrofit.create(FlightService::class.java)
    }

    @Provides
    @Singleton
    fun provideFlightService(context: Context, retrofit: Retrofit): FlightService {
        return doProvideFlightService(context, retrofit)
    }
}


@Module
class MockNetworkServiceModule() : NetworkServicesModule() {
    /**
     * Need this to be able to mock a service in a production app
     */
    override fun doProvideFlightService(context: Context, retrofit: Retrofit): FlightService {
        return MockFlightService(context, super.doProvideFlightService(context, retrofit))
    }

鉴于您有一个这样定义的组件:

@Component(
    modules = [
        NetworkServicesModule::class,
    ]
)
interface BaseComponent {
    fun provideFlightService(): FlightService

@Component.Builder
    interface Builder {
        fun networkServicesModule(networkServicesModule: NetworkServicesModule): Builder

        fun build(): BaseComponent
    }
}

您可以根据您的应用程序变体/风格或您更喜欢它的方式选择一些网络模块提供程序:


/**
 * Provides mock networking services module
 */
interface NetworkServicesModuleProvider {
    /**
     * Provides networking service module
     */
    fun get() : NetworkServicesModule = NetworkServiceModule()
}

/**
 * Provides mock networking services module
 */
object MockNetworkServicesModuleProvider : NetworkServicesModuleProvider {
    /**
     * Provides networking service module
     */
    override fun get() : NetworkServicesModule = MockNetworkServiceModule()
}

然后是时候构建您的组件调用:


val networkServicesProvider: NetworkServicesModuleProvider = ...

DaggerBaseComponent.builder()
            .networkServicesModule(networkServicesProvider.get())
            .build()
于 2021-09-30T09:43:24.907 回答
0

我有Roboletric 3.+的解决方案。

我有 MainActivity 我想在创建时不进行注入测试:

public class MainActivity extends BaseActivity{

  @Inject
  public Configuration configuration;

  @Inject
  public AppStateService appStateService;

  @Inject
  public LoginService loginService;

  @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.processIntent(getIntent()); // this is point where pass info from test
      super.onCreate(savedInstanceState)
    ...
  }
  ...
 }

接下来是我的 BaseActivty:

public class BaseActivity extends AppCompatActivity {

  protected Logger mLog;

  protected boolean isTestingSession = false; //info about test session


  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
      if (!isTestingSession) { // check if it is in test session, if not enable injectig
          AndroidInjection.inject(this);
      }
      super.onCreate(savedInstanceState);
  }

  // method for receive intent from child and scaning if has item TESTING with true
  protected void processIntent(Intent intent) {
    if (intent != null && intent.getExtras() != null) {
        isTestingSession = intent.getExtras().getBoolean("TESTING", false);
    }
  }

最后是我的测试课:

@Before
public void setUp() throws Exception {
  ...
  // init mocks...
   loginServiceMock = mock(LoginService.class);
   locServiceMock = mock(LocationClientService.class);
   fakeConfiguration = new ConfigurationUtils(new ConfigurationXmlParser());
   fakeConfiguration.save(FAKE_XML_CONFIGURATION);
   appStateService = new AppStateService(fakeConfiguration, locServiceMock, RuntimeEnvironment.application);

   // prepare activity
   Intent intent = new Intent(RuntimeEnvironment.application, MainActivity.class);
   intent.putExtra("TESTING", true);
   ActivityController<MainActivity> activityController = Robolectric.buildActivity(MainActivity.class, intent); // place to put bundle with extras

    // get the activity instance
    mainActivity = activityController.get();


    // init fields which should be injected
    mainActivity.appStateService = appStateService;
    mainActivity.loginService = loginServiceMock;
    mainActivity.configuration = fakeConfiguration;


    // and whoala 
    // now setup your activity after mock injection
    activityController.setup();

    // get views etc..
    actionButton = mainActivity.findViewById(R.id.mainButtonAction);
    NavigationView navigationView = mainActivity.findViewById(R.id.nav_view);

  ....
  }
于 2018-10-31T07:06:54.553 回答
-5

使用 Dagger2,您可以使用生成的构建器 api 将特定模块(那里的 TestModule)传递给组件。

ApplicationComponent appComponent = Dagger_ApplicationComponent.builder()
                .helloModule(new TestModule())
                .build();

请注意,Dagger_ApplicationComponent 是一个带有新的@Component 注解的生成类。

于 2014-11-15T12:04:19.960 回答