Как протестировать действие, которое использует ContentProvider, не затрагивая производственную базу данных?

ПРОБЛЕМА

У меня есть два класса Android, которые я хочу протестировать:

  • CommentContentProvider, который расширяет ContentProvider и поддерживается SQLiteDatabase.
  • CommentActivity, который расширяет Activity и обращается к CommentContentProvider косвенно через ContentResolver.

В настоящее время у меня есть два тестовых класса:

  • CommentContentProviderTest, который расширяет ProviderTestCase2<CommentContentProvider> и использует MockContentResolver. Это прекрасно работает.
  • CommentActivityTest, который расширяет ActivityInstrumentationTestCase2<CommentActivity>. Это работает нормально, за исключением частей CommentActivity, которые обращаются к CommentContentProvider.

Проблема в том, что когда CommentActivity обращается к CommentContentProvider, он делает это через стандартный ContentResolver:

ContentResolver resolver = getContentResolver();
Cursor cursor = resolver().query(...);

Таким образом, когда запускается CommentActivityTest, он запускает CommentActivity, который осуществляет доступ (чтение и запись) к производственной базе данных, как показано в двух строках выше.

Мой вопрос заключается в том, как заставить CommentActivity использовать стандартный ContentResolver в производстве, но MockContentResolver во время тестирования.

СВЯЗАННЫЕ ВОПРОСЫ

ВОЗМОЖНЫЕ РЕШЕНИЯ

Было бы неплохо, если бы я мог внедрить ContentResolver (возможно, MockContentResolver или RenamingDelegatingContext) через Намерение, которое запускает CommentActivity, но я не могу этого сделать, так как Context не являются Parcelable.

Какой из следующих вариантов лучше, или есть лучший вариант?

ВАРИАНТ 1

Добавьте флаг отладки в Intent, который запускает CommentActivity:

public class CommentActivity extends Activity {
    public static final String DEBUG_MODE = "DEBUG MODE";
    private ContentResolver mResolver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        :
        // If the flag is not present, debugMode will be set to false.
        boolean debugMode = getIntent().getBooleanExtra(DEBUG_MODE, false);
        if (debugMode) {
            // Set up MockContentResolver or DelegatingContextResolver...
        } else {
            mResolver = getContentResolver();
        }
        :
    }

Мне не нравится этот вариант, потому что я не люблю размещать код, связанный с тестами, в своих нетестовых классах.

ВАРИАНТ 2

Используйте шаблон абстрактной фабрики, чтобы передать класс Parcelable, который предоставляет реальный ContentProvider или MockContentProvider:

public class CommentActivity extends Activity {
    public static final String FACTORY = "CONTENT RESOLVER FACTORY";
    private ContentResolver mResolver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        :
        ContentResolverFactory factory = getIntent().getParcelableExtra(FACTORY);
        mResolver = factory.getContentResolver(this);
        :
    }

где у меня также есть:

public abstract class ContentResolverFactory implements Parcelable {
    public abstract ContentResolver getContentResolver(Context context);
}

public abstract class RealContentResolverFactory extends ContentResolverFactory
    public ContentResolver getContentResolver(Context context) {
        return context.getContextResolver();
    }
}

public abstract class MockContentResolverFactory extends ContentResolverFactory
    public ContentResolver getContentResolver(Context context) {
        MockContentResolver resolver = new MockContentResolver();
        // Set up MockContentResolver...
        return resolver;
    }
}

В рабочей среде я передаю (через намерение) экземпляр RealContentResolverFactory, а в тесте я передаю экземпляр MockContentResolverFactory. Поскольку ни у одного из них нет состояния, они легко Parcelable/Serializable.

Меня беспокоит этот подход, потому что я не хочу быть "этим парнем" кто злоупотребляет шаблонами проектирования, когда существуют более простые подходы.

ВАРИАНТ 3

Добавьте следующий метод в CommentActivity:

public void static setContentResolver(ContentResolver) {
  :
}

Это чище, чем вариант 1, поскольку он помещает создание ContentResolver вне CommentActivity, но, как и вариант 1, требует изменения тестируемого класса.

ВАРИАНТ 4

Пусть CommentActivityTest расширит ActivityUnitTestCase<CommentActivity> вместо ActivityInstrumentationTestCase2<CommentActivity>. Это позволяет мне установить контекст CommentActivity через setActivityContext() . Контекст, который я передаю, переопределяет обычный getContentResolver() для использования MockContentResolver (который я инициализирую в другом месте).

private class MyContext extends RenamingDelegatingContext {
    MyContext(Context context) {
        super(context, FILE_PREFIX);
    }

    @Override
    public ContentResolver getContentResolver() {
        return mResolver;
    }
}

Это работает и не требует модификации тестируемого класса, но добавляет сложности, поскольку ActivityUnitTestCase<CommentActivity>.startActivity() нельзя вызывать в методе setUp() согласно API.

Еще одно неудобство заключается в том, что действие должно быть протестировано в сенсорном режиме, и setActivityInitialTouchMode(boolean) определен в ActivityInstrumentationTestCase2<T>, но не ActivityUnitTestCase<T>.

FWIW, я немного одержим тем, чтобы сделать это правильно, потому что я буду представлять это на уроке разработки Android, который я преподаю.


person Ellen Spertus    schedule 22.03.2014    source источник
comment
Я смущен тем, как работает вариант 2. Как фактическая фабрика выбирается в prod vs test?   -  person NamshubWriter    schedule 24.03.2014
comment
@NamshubWriter, спасибо за чтение. Я добавил к вопросу: в производстве я передаю (через намерение) экземпляр RealContentResolverFactory, а в тесте я передаю экземпляр MockContentResolverFactory. Поскольку ни у одного из них нет состояния, они легко Parcelable/Serializable.   -  person Ellen Spertus    schedule 24.03.2014
comment
Есть небольшая орфографическая ошибка. В варианте 2 getContextResolver должен быть getContentResolver.   -  person evgeny.myasishchev    schedule 12.04.2015
comment
В той же ситуации. Мне нравится вариант 3, но, к сожалению, сохранение его в статическом поле приводит к утечке памяти (как и предупреждалось в AS 2.2).   -  person sargas    schedule 01.06.2016
comment
@EllenSpertus: Вы когда-нибудь находили хорошее решение?   -  person MWB    schedule 20.10.2018
comment
@MWB Нет, не видел.   -  person Ellen Spertus    schedule 20.10.2018
comment
@EllenSpertus: я только что задал свой вопрос на эту тему: "> stackoverflow.com/questions/52905933/   -  person MWB    schedule 20.10.2018


Ответы (1)


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

person NamshubWriter    schedule 30.03.2014
comment
Спасибо за ответ. Тишина была оглушительной. :-) - person Ellen Spertus; 30.03.2014