Как переопределить модуль / зависимость в модульном тесте с помощью 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);
}
что такое Кинжал 2.0 эквивалент выше?
вы можете увидеть мой проект и его модульный тест здесь на GitHub.
7 ответов:
вероятно, это больше обходной путь, что правильная поддержка переопределения тестового модуля, но это позволяет переопределить производственные модули с тестовым один. Фрагменты кода ниже показывает простой случай, когда у вас есть только один компонент и один модуль, но это должно работать для любого сценария. Это требует много шаблонного и кодового повторения, поэтому имейте это в виду. Я уверен, что в будущем будет лучший способ добиться этого.
я также создал проект с примерами для Эспрессо и 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
С "тестовая строка". Мы должны установить тестовый компонент вApp
передMainActivity
создается - потому чтоStringHolder
вводят вonCreate
обратный.в компонентах Кинжала v2.0.0 можно расширить другие интерфейсы. Мы можем использовать это для создания нашего
TestAppComponent
, который выходитAppComponent
.@Component(modules = TestStringHolderModule.class) interface TestAppComponent extends AppComponent { }
теперь мы можем определить наши тестовые модули, например
TestStringHolderModule
. Последний шаг-установить тестовый компонент, используя ранее добавленный метод setter вApp
класса. Важно сделать это до создания.((App) application).setTestComponent(mTestAppComponent);
кофе
для эспрессо я создал custom
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(...) } }
Как справедливо говорит @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);
Я создал в этом суть Показать полностью образец.
EDIT
оказывается, вы можете сделать это даже без частичного макета, например:
MyComponent component = DaggerMyComponent.builder() .myModule(new MyModule() { @Override public String provideString() { return "mocked string"; } }) .build(); app.setComponent(component);
обходной путь, предложенный @tomrozb, очень хорош и поставил меня на правильный путь, но моя проблема заключалась в том, что он выставил
setTestComponent()
метод в производствеApplication
класса. Я смог заставить это работать немного по-другому, так что мое производственное приложение не должно ничего знать о моей тестовой среде.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); } }
вы также будете иметь чтобы обновить ваш
testInstrumentationRunner
gradle, вот так:testInstrumentationRunner "com.mypackage.TestRunner"
и если вы используете Android Studio, вам также нужно будет нажать кнопку изменить конфигурацию в меню Выполнить и ввести имя вашего тестового бегуна в разделе "Specific instrumentation runner".
и это все! Надеюсь, эта информация кому-то поможет :)
кажется, я нашел еще один способ, и он работает до сих пор.
во-первых, интерфейс компонента, который не является самим компонентом:
компонент 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; } }
ЭТОТ ОТВЕТ УСТАРЕЛ. ЧИТАЙТЕ НИЖЕ В ПРАВКЕ.
к сожалению, ты не может распространяться из модуля, или вы получите следующую ошибку компиляции:
Error:(24, 21) error: @Provides methods may not override another method. Overrides: Provides retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.myServerEndpoint()
означает, что вы не можете просто расширить "макет модуля" и заменить исходный модуль. Нет, это не так просто. И учитывая, что вы разрабатываете свои компоненты таким образом, что он напрямую связывает модули по классам, вы не можете просто сделать "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 'ы ответ.
можете ли вы, ребята, проверить мое решение, я включил пример подкомпонента: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()); }
С помощью Dagger2 вы можете передать определенный модуль (TestModule там) компоненту с помощью сгенерированного API builder.
ApplicationComponent appComponent = Dagger_ApplicationComponent.builder() .helloModule(new TestModule()) .build();
обратите внимание, что Dagger_ApplicationComponent-это сгенерированный класс с новой аннотацией @Component.