Это правильный способ использовать Dagger 2 для Android-приложения в unit test для переопределения зависимостей с помощью mocks/fakes?
Для "обычного" Java-проекта, переопределяющего зависимости в модульных тестах с помощью mock/fake, легко. Вы должны просто создать свой Кинжал и передать его классу 'main', который будет управлять вашим приложением.
Для Android вещи не такие простые, и я долго искал достойный пример, но я не смог найти, поэтому мне пришлось создать свою собственную реализацию и я буду очень благодарен, что обратная связь - это правильный способ использования Dagger 2 или более простой/более элегантный способ переопределить зависимости.
Здесь объяснение (источник проекта можно найти в github):
Учитывая, что у нас есть простое приложение, которое использует Dagger 2 с одним компонентом кинжала с одним модулем, мы хотим создать тесты для Android, которые используют JUnit4, Mockito и Espresso:
В классе MyApp
Application
компонент/инжектор инициализируется следующим образом:
public class MyApp extends Application {
private MyDaggerComponent mInjector;
public void onCreate() {
super.onCreate();
initInjector();
}
protected void initInjector() {
mInjector = DaggerMyDaggerComponent.builder().httpModule(new HttpModule(new OkHttpClient())).build();
onInjectorInitialized(mInjector);
}
private void onInjectorInitialized(MyDaggerComponent inj) {
inj.inject(this);
}
public void externalInjectorInitialization(MyDaggerComponent injector) {
mInjector = injector;
onInjectorInitialized(injector);
}
...
В приведенном выше коде:
Обычный запуск приложения идет через tough onCreate()
, который вызывает initInjector()
, который создает инжектор, а затем вызывает onInjectorInitialized()
.
Метод externalInjectorInitialization()
может быть вызван модульными тестами для set
инжектора из внешнего источника, то есть a unit test.
До сих пор так хорошо.
Посмотрите, как выглядят вещи на стороне модулей:
Нам нужно создать вызовы MyTestApp, которые расширяют класс MyApp и переопределяют initInjector
пустым методом, чтобы избежать создания двойного инжектора (потому что мы создадим новый в нашем unit test):
public class MyTestApp extends MyApp {
@Override
protected void initInjector() {
// empty
}
}
Затем мы должны как-то заменить оригинальный MyApp MyTestApp. Это делается через пользовательский тестовый бегун:
public class MyTestRunner extends AndroidJUnitRunner {
@Override
public Application newApplication(ClassLoader cl,
String className,
Context context) throws InstantiationException,
IllegalAccessException,
ClassNotFoundException {
return super.newApplication(cl, MyTestApp.class.getName(), context);
}
}
... где в newApplication()
мы эффективно заменяем исходный класс приложения тестовым.
Затем мы должны указать структуру тестирования, которую у нас есть, и хотим использовать наш пользовательский тестовый бегун, поэтому в build.gradle добавим:
defaultConfig {
...
testInstrumentationRunner 'com.bolyartech.d2overrides.utils.MyTestRunner'
...
}
Когда выполняется unit test, наш оригинальный MyApp
заменяется на MyTestApp
. Теперь нам нужно создать и предоставить нашему компоненту/инжектору mocks/fakes в приложение с помощью externalInjectorInitialization()
. Для этого мы расширяем обычный ActivityTestRule:
@Rule
public ActivityTestRule<Act_Main> mActivityRule = new ActivityTestRule<Act_Main>(
Act_Main.class) {
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
OkHttpClient mockHttp = create mock OkHttpClient
MyDaggerComponent injector = DaggerMyDaggerComponent.
builder().httpModule(new HttpModule(mockHttp)).build();
MyApp app = (MyApp) InstrumentationRegistry.getInstrumentation().
getTargetContext().getApplicationContext();
app.externalInjectorInitialization(injector);
}
};
а затем мы проводим наш тест обычным способом:
@Test
public void testHttpRequest() throws IOException {
onView(withId(R.id.btn_execute)).perform(click());
onView(withId(R.id.tv_result))
.check(matches(withText(EXPECTED_RESPONSE_BODY)));
}
Вышеуказанный метод переопределения (модуля) работает, но для каждого теста требуется создать один тестовый класс, чтобы иметь возможность предоставлять отдельное правило/(настройка макетов) для каждого теста. Я подозреваю/думаю/надеюсь, что есть более простой и элегантный способ. Есть?
Этот метод во многом основан на ответе @tomrozb на на этот вопрос. Я просто добавил логику, чтобы избежать создания двойного инжектора.
Ответы
Ответ 1
1. Вложение по зависимостям
Следует отметить две вещи:
- Компоненты могут обеспечить себя
- Если вы можете вставить его один раз, вы можете снова ввести его (и переопределить старые зависимости)
То, что я делаю, просто вводит из моего тестового примера по старым зависимостям. Поскольку ваш код чист, и все правильно определено, ничего не должно идти не так, как правильно?
Следующие действия будут работать только в том случае, если вы не полагаетесь на Глобальное состояние, поскольку изменение компонента приложения во время выполнения не будет работать, если вы сохраните ссылки на старый в каком-либо месте. Как только вы создадите свой следующий Activity
, он получит новый компонент приложения, и ваши тестовые зависимости будут предоставлены.
Этот метод зависит от правильной обработки областей. Завершение и перезапуск мероприятия должны воссоздать свои зависимости. Таким образом, вы можете переключать компоненты приложения, когда нет активности или перед запуском нового.
В вашем тестовом сценарии просто создайте свой компонент по своему усмотрению
// in @Test or @Before, just inject 'over' the old state
App app = (App) InstrumentationRegistry.getTargetContext().getApplicationContext();
AppComponent component = DaggerAppComponent.builder()
.appModule(new AppModule(app))
.build();
component.inject(app);
Если у вас есть приложение вроде следующего...
public class App extends Application {
@Inject
AppComponent mComponent;
@Override
public void onCreate() {
super.onCreate();
DaggerAppComponent.builder().appModule(new AppModule(this)).build().inject(this);
}
}
... он будет вводить себя и любые другие зависимости, которые вы определили в своем Application
. Затем любой последующий вызов получит новые зависимости.
2. Используйте другую конфигурацию и приложение
Вы можете выбрать конфигурацию, которая будет использоваться с вашим контрольным тестом:
android {
...
testBuildType "staging"
}
Использование слияния ресурсов gradle дает вам возможность использовать несколько разных версий вашего App
для разных типов сборки.
Переместите класс Application
из исходной папки main
в папки debug
и release
. gradle будет скомпилировать правильный набор источников в зависимости от конфигурации. Затем вы можете изменить свою отладочную версию и версию своего приложения в соответствии с вашими потребностями.
Если вы не хотите иметь разные классы Application
для отладки и выпуска, вы можете сделать еще один buildType
, используемый только для ваших контрольных тестов. Тот же принцип применяется: Дублируйте класс Application
в каждую папку с исходным кодом или вы получите ошибки компиляции. Так как тогда вам понадобится иметь тот же класс в каталоге debug
и rlease
, вы можете сделать другой каталог содержащим класс, используемый для отладки и выпуска. Затем добавьте каталог, используемый для ваших отладочных и релизных наборов.
Ответ 2
Существует более простой способ сделать это, даже Dagger 2 docs упоминают об этом, но они не делают это очень очевидным. Вот фрагмент из документации.
@Provides static Pump providePump(Thermosiphon pump) {
return pump;
}
Термосифон реализует насос и везде, где требуется насос. Кинжал вводит термосифон.
Возвращаясь к вашему примеру. Вы можете создать модуль со статическим логическим элементом данных, который позволяет динамически переключаться между реальными и имитирующими тестовыми объектами, например.
@Module
public class HttpModule {
private static boolean isMockingHttp;
public HttpModule() {}
public static boolean mockHttp(boolean isMockingHttp) {
HttpModule.isMockingHttp = isMockingHttp;
}
@Provides
HttpClient providesHttpClient(OkHttpClient impl, MockHttpClient mockImpl) {
return HttpModule.isMockingHttp ? mockImpl : impl;
}
}
HttpClient может быть суперклассом, который является расширенным, или интерфейсом, который реализуется OkHttpClient и MockHttpClient. Кинжал автоматически построит требуемый класс и добавит его внутренние зависимости точно так же, как Thermosiphon.
Чтобы высмеять ваш HttpClient, просто вызовите HttpModule.mockHttp(true)
, прежде чем ваши зависимости будут введены в ваш код приложения.
Преимущества этого подхода заключаются в следующем:
- Не нужно создавать отдельные тестовые компоненты, так как макеты вводятся на уровне модуля.
- Код приложения остается нетронутым.