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

Это правильный способ использовать 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 на на этот вопрос. Я просто добавил логику, чтобы избежать создания двойного инжектора.

4b9b3361

Ответ 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), прежде чем ваши зависимости будут введены в ваш код приложения.

Преимущества этого подхода заключаются в следующем:

  • Не нужно создавать отдельные тестовые компоненты, так как макеты вводятся на уровне модуля.
  • Код приложения остается нетронутым.