Подтвердить что ты не робот

Как вы переопределяете модуль/зависимость в unit test с помощью Dagger 2.0?

У меня есть простая активность Android с одной зависимостью. Я ввожу зависимость в активность onCreate следующим образом:

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

В моем ActivityUnitTestCase я хочу переопределить зависимость с помощью mockito mock. Я предполагаю, что мне нужно использовать тестовый модуль, который предоставляет макет, но я не могу понять, как добавить этот модуль в граф объектов.

В кинжале 1.x это, по-видимому, сделано с чем-то вроде этого:

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

Что такое Dagger 2.0 эквивалент выше?

Вы можете увидеть мой проект и unit test здесь, на GitHub.

4b9b3361

Ответ 1

Вероятно, это скорее обходной путь, который обеспечивает надлежащую поддержку для тестирования модуля, но он позволяет переопределять производственные модули с тестовым. В приведенных ниже фрагментах кода показан простой случай, когда у вас есть только один компонент и один модуль, но это должно работать для любого сценария. Для этого требуется много повторений шаблонов и кодов, поэтому имейте это в виду. Я уверен, что в будущем будет лучший способ достичь этого.

Я также создал проект с примерами для 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 содержит значение "Release string". Этот объект вводится в 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 "Test string". Мы должны установить тестовый компонент в классе App до создания MainActivity, потому что StringHolder вводится в обратном вызове onCreate.

В Dagger v2.0.0 компоненты могут расширять другие интерфейсы. Мы можем использовать это, чтобы создать наш TestAppComponent, который расширяет AppComponent.

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

}

Теперь мы можем определить наши тестовые модули, например. TestStringHolderModule. Последний шаг - установить тестовый компонент с использованием ранее добавленного метода setter в классе App. Важно сделать это до того, как будет создана активность.

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

Эспрессо

Для Espresso я создал пользовательский ActivityTestRule, который позволяет обменивать компонент до создания активности. Вы можете найти код для DaggerActivityTestRule здесь.

Пример теста с эспрессо:

@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

Это намного проще с 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(...)
    }
}

Ответ 2

Как справедливо говорит @EpicPandaForce, вы не можете расширять модули. Тем не менее, я придумал проворное обходное решение для этого, которое, я думаю, избегает большого количества шаблонов, от которых страдают другие примеры.

Трюк к "расширению" модуля - создание частичного макета и макетирование методов провайдера, которые вы хотите переопределить.

Используя Mockito:

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);

Ответ 3

Обходной путь, предложенный @tomrozb, очень хорош и поставил меня на правильный путь, но моя проблема заключалась в том, что он раскрыл метод setTestComponent() в классе PRODUCTION Application. Мне удалось заставить это работать немного по-другому, так что моему производственному приложению ничего не нужно знать о моей тестовой среде.

TL; DR - Расширьте свой класс приложения тестовым приложением, которое использует ваш тестовый компонент и модуль. Затем создайте собственный тестовый бегун, который запускается в тестовом приложении, а не в рабочем приложении.


EDIT: этот метод работает только для глобальных зависимостей (обычно с @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 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);
    }
}

Вам также потребуется обновить testInstrumentationRunner gradle, например:

testInstrumentationRunner "com.mypackage.TestRunner"

И если вы используете Android Studio, вам также нужно щелкнуть "Редактировать конфигурацию" в меню "Запуск" и ввести имя вашего тестового бегуна в разделе "Специфический контролер".

И это! Надеюсь, эта информация поможет кому-то:)

Ответ 4

Кажется, я нашел еще один способ, и он работает до сих пор.

Сначала интерфейс компонента, который не является самим компонентом:

MyComponent.java

interface MyComponent {
    Foo provideFoo();
}

Тогда у нас есть два разных модуля: фактический и тестовый.

MyModule.java

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

TestModule.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, если они не установлены напрямую извне. Вот так:

TestModule.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;
    }
}

Ответ 5

ЭТО ОТВЕТ ОБОЛОЧЕН. ПРОЧИТАЙТЕ НИЖЕ ИЗМЕНЕНИЯ.

Достаточно неудобно, вы не можете распространяться из модуля, или вы получите следующую ошибку компиляции:

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();
    }
}

EDIT: Как видно из сообщения об ошибке, вы НЕ МОЖЕТЕ переопределить другой метод с помощью аннотированного метода @Provides, но это не означает, что вы не можете переопределить аннотированный метод @Provides: (

Все это волшебство было напрасно! Вы можете просто расширить модуль, не помещая @Provides в метод, и он работает... Обратитесь к ответу @vaughandroid.

Ответ 6

Можете ли вы, ребята, проверить мое решение, я включил пример подкомпонента: https://github.com/nongdenchet/android-mvvm-with-tests. Спасибо @vaughandroid, я позаимствовал ваши основные методы. Вот главный момент:

  • Я создаю класс для создания подкомпонента. Мое пользовательское приложение также будет содержать экземпляр этого класса:

    // 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);
      ...
     }
    }
    
  • У меня есть пользовательская TestApplication, которая расширяет класс MyApplication выше. Этот класс содержит два метода для замены корневого компонента и построителя:

    public class TestApplication extends MyApplication {
     public void setComponent(AppComponent appComponent) {
      this.mAppComponent = appComponent;
     }
    
     public void setComponentBuilder(ComponentBuilder componentBuilder) {
      this.mComponentBuilder = componentBuilder;
     }
    }    
    
  • Наконец, я попытаюсь высмеять или заглушить зависимость модуля и построителя, чтобы обеспечить фальшивую зависимость от активности:

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

Ответ 7

С помощью Dagger2 вы можете передать конкретный модуль (TestModule) к компоненту с помощью сгенерированного конструктора api.

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

Обратите внимание, что Dagger_ApplicationComponent является сгенерированным классом с новой аннотацией @Component.