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

Получая кинжал, чтобы вводить ложные объекты при выполнении функционального тестирования espresso для Android

Недавно я перешел с кинжалом, потому что концепция DI имеет смысл. Один из лучших "побочных продуктов" DI (как Джейк Уортон, поставленный в одной из его презентаций) легче тестировать.

Итак, теперь я в основном использую эспрессо для выполнения некоторых функциональных тестов, и я хочу иметь возможность вводить фиктивные/макетные данные в приложение и показывать их активность. Я предполагаю, что это одно из самых больших преимуществ DI, это должно быть относительно просто спросить. Однако по какой-то причине я не могу обернуть вокруг себя голову. Любая помощь приветствуется. Вот что я до сих пор (я написал пример, который отражает мою текущую настройку):

public class MyActivity
    extends MyBaseActivity {

    @Inject Navigator _navigator;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyApplication.get(this).inject(this);

        // ...

        setupViews();
    }

    private void setupViews() {
        myTextView.setText(getMyLabel());
    }

    public String getMyLabel() {
        return _navigator.getSpecialText(); // "Special Text"
    }
}

Это мои модули кинжалов:

// Navigation Module

@Module(library = true)
public class NavigationModule {

    private Navigator _nav;

    @Provides
    @Singleton
    Navigator provideANavigator() {
        if (_nav == null) {
            _nav = new Navigator();
        }
        return _nav;
    }
}

// App level module

@Module(
    includes = { SessionModule.class, NavigationModule.class },
    injects = { MyApplication.class,
                MyActivity.class,
                // ...
})
public class App {
    private final Context _appContext;
    AppModule(Context appContext) {
        _appContext = appContext;
    }
    // ...
}

В моем тесте эспрессо я пытаюсь вставить макет модуля следующим образом:

public class MyActivityTest
    extends ActivityInstrumentationTestCase2<MyActivity> {

    public MyActivityTest() {
        super(MyActivity.class);
    }

  @Override
  public void setUp() throws Exception {
      super.setUp();
      ObjectGraph og = ((MyApplication) getActivity().getApplication()).getObjectGraph().plus(new TestNavigationModule());
      og.inject(getActivity());
  }

    public void test_SeeSpecialText() {
        onView(withId(R.id.my_text_view)).check(matches(withText(
            "Special Dummy Text)));
    }

    @Module(includes = NavigationModule.class,
            injects = { MyActivityTest.class, MyActivity.class },
            overrides = true,
            library = true)
    static class TestNavigationModule {

        @Provides
        @Singleton
        Navigator provideANavigator() {
            return new DummyNavigator(); // that returns "Special Dummy Text"
        }
    }
}

Это не работает вообще. Мои тесты эспрессо выполняются, но TestNavigationModule полностью игнорируется... arr...: (

Что я делаю неправильно? Есть ли лучший подход к издевательствам модулей с Espresso. Я искал и видел примеры использования Robolectric, Mockito и т.д. Но я просто хочу чистые тесты эспрессо и мне нужно поменять модуль на мой макет. Как я должен это делать?

EDIT:

Итак, я пошел с подходом @user3399328 к определению определения статического тестового модуля, проверяя значение null и затем добавляя его в свой класс Application. Тем не менее, я все еще не получаю свою тестовую версию класса. У меня есть чувство, хотя, вероятно, что-то не так с определением модуля тестирования кинжала, а не с моим жизненным циклом эспрессо. Причина, по которой я делаю предположение, заключается в том, что я добавляю операторы отладки и обнаруживаю, что статический тестовый модуль не является пустым во время инъекции в классе приложения. Не могли бы вы указать мне направление того, что я могу сделать неправильно. Вот фрагменты кода моих определений:

MyApplication:

@Override
public void onCreate() {
    // ...
    mObjectGraph = ObjectGraph.create(Modules.list(this));
    // ...   
}

Модули:

public class Modules {

    public static List<Object> _testModules = null;

    public static Object[] list(MyApplication app) {
        //        return new Object[]{ new AppModule(app) };
        List<Object> modules = new ArrayList<Object>();
        modules.add(new AppModule(app));

        if (_testModules == null) {
            Log.d("No test modules");
        } else {
            Log.d("Test modules found");
        }

        if (_testModules != null) {
            modules.addAll(_testModules);
        }

        return modules.toArray();
    }
}   

Модифицированный тестовый модуль в моем классе:

@Module(overrides = true, library = true)
public static class TestNavigationModule {

    @Provides
    @Singleton
    Navigator provideANavigator()() {
        Navigator navigator = new Navigator();
        navigator.setSpecialText("Dummy Text");
        return navigator;
    }
}
4b9b3361

Ответ 1

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

Есть два способа заставить это работать.

Быстрый способ: сделать общедоступную статическую переменную в своей деятельности, чтобы тест мог назначить модуль переопределения и иметь действительный код операции, который всегда включает этот модуль, если он не является нулевым (что произойдет только в тестах). Это похоже на мой ответ здесь только для вашего базового класса активности, а не для приложения.

Чем длиннее, возможно, лучший способ: реорганизовать свой код так, чтобы вся инъекция активности (и, что более важно, создание графика) происходила в одном классе, что-то вроде ActivityInjectHelper. В тестовом пакете создайте еще один класс с именем ActivityInjectHelper с тегом точно такой же, который реализует одни и те же методы, за исключением того, что вы используете ваши тестовые модули. Поскольку сначала загружаются тестовые классы, ваше приложение будет выполняться с тестом ActivityInjectHelper. Снова это похоже на мой ответ здесь только для другого класса.

UPDATE:

Я вижу, что вы разместили больше кода, и это близко к работе, но не сигары. Для обоих видов деятельности и приложений тестовый модуль должен быть прокручен до начала работы onCreate(). При работе с графиками объектов деятельности в любое время до теста getActivity() отлично. При работе с приложениями это немного сложнее, потому что onCreate() уже вызван запуском timeUp(). К счастью, выполнение этого в конструкторе тестов работает - приложение не было создано в этот момент. Я кратко упомянул об этом в моей первой ссылке.

Ответ 2

С Dagger 2 и Espresso 2 вещи действительно улучшились. Вот как выглядит тестовый пример. Обратите внимание, что ContributorsModel предоставляется кинжалом. Полная демо-версия доступна здесь: https://github.com/pmellaaho/RxApp

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

ContributorsModel mModel;

@Singleton
@Component(modules = MockNetworkModule.class)
public interface MockNetworkComponent extends RxApp.NetworkComponent {
}

@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
        MainActivity.class,
        true,     // initialTouchMode
        false);   // launchActivity.

@Before
public void setUp() {
    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    RxApp app = (RxApp) instrumentation.getTargetContext()
            .getApplicationContext();

    MockNetworkComponent testComponent = DaggerMainActivityTest_MockNetworkComponent.builder()
            .mockNetworkModule(new MockNetworkModule())
            .build();
    app.setComponent(testComponent);
    mModel = testComponent.contributorsModel();
}

@Test
public void listWithTwoContributors() {

    // GIVEN
    List<Contributor> tmpList = new ArrayList<>();
    tmpList.add(new Contributor("Jesse", 600));
    tmpList.add(new Contributor("Jake", 200));

    Observable<List<Contributor>> testObservable = Observable.just(tmpList);

    Mockito.when(mModel.getContributors(anyString(), anyString()))
            .thenReturn(testObservable);

    // WHEN
    mActivityRule.launchActivity(new Intent());
    onView(withId(R.id.startBtn)).perform(click());

    // THEN
    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0))
            .check(matches(hasDescendant(withText("Jesse"))));

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0))
            .check(matches(hasDescendant(withText("600"))));

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1))
            .check(matches(hasDescendant(withText("Jake"))));

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1))
            .check(matches(hasDescendant(withText("200"))));
}

Ответ 3

Призыв getActivity фактически начнет вашу деятельность, вызывая onCreate в этом процессе, что означает, что вы не будете получать ваши тестовые модули, добавленные к графику во время, которое будет использоваться. Используя activityInstrumentationTestcase2, вы не можете правильно вводить информацию в области действия. Я работал над этим, используя мое приложение для предоставления зависимостей от моих действий, а затем внедряю в него макетные объекты, которые будут использовать эти действия. Он не идеален, но он работает. Вы можете использовать шину событий, такую ​​как Otto, чтобы помочь обеспечить зависимости.

Ответ 4

EDIT: ниже в форме сообщения http://systemdotrun.blogspot.co.uk/2014/11/android-testing-with-dagger-retrofit.html

Чтобы проверить Activity с помощью эспрессо + кинжал, я сделал ниже

Вдохновленный ответом от @user3399328 У меня есть класс DaggerHelper внутри моего класса Application, который позволяет тестовому примеру переопределить @Provider с помощью Test @Modules, которые поставляют mocks. Пока

1) Это делается до вызова testCases getActivity() (поскольку мой инъекционный вызов происходит в моей активности внутри Activity.onCreate)

2) tearDown удаляет тестовые модули из графа объектов.

Примеры ниже.

Примечание: это не идеально, так как это связано с аналогичными ошибками использования методов factory для IoC, но по крайней мере таким образом его единственный единственный вызов в tearDown(), чтобы вернуть систему в нормальное состояние.

DaggerHelper внутри моего класса Application

public static class DaggerHelper
{
    private static ObjectGraph sObjectGraph;

    private static final List<Object> productionModules;

    static
    {
        productionModules = new ArrayList<Object>();
        productionModules.add(new DefaultModule());
    }

    /**
     * Init the dagger object graph with production modules
     */
    public static void initProductionModules()
    {
        initWithModules(productionModules);
    }

    /**
     * If passing in test modules make sure to override = true in the @Module annotation
     */
    public static void initWithTestModules(Object... testModules)
    {
        initWithModules(getModulesAsList(testModules));
    }

    private static void initWithModules(List<Object> modules)
    {
        sObjectGraph = ObjectGraph.create(modules.toArray());
    }

    private static List<Object> getModulesAsList(Object... extraModules)
    {
        List<Object> allModules = new ArrayList<Object>();
        allModules.addAll(productionModules);
        allModules.addAll(Arrays.asList(extraModules));
        return allModules;
    }

    /**
     * Dagger convenience method - will inject the fields of the passed in object
     */
    public static void inject(Object object) {
        sObjectGraph.inject(object);
    }
}

Мой тестовый модуль внутри моего тестового класса

@Module (
        overrides = true,
        injects = ActivityUnderTest.class
)
static class TestDataPersisterModule {
    @Provides
    @Singleton
    DataPersister provideMockDataPersister() {
        return new DataPersister(){
            @Override
            public void persistDose()
            {
                throw new RuntimeException("Mock DI!"); //just a test to see if being called
            }
        };
    }
}

Метод тестирования

public void testSomething()
{ 
     MyApp.DaggerHelper.initWithTestModules(new TestDataPersisterModule());
     getActivity();
     ...
 }

Срыв вниз

@Override
public void tearDown() throws Exception
{
    super.tearDown();
    //reset
    MyApp.DaggerHelper.initProductionModules();
}