Как издеваться над getApplicationContext

У меня есть приложение, которое хранит информацию о приложении. Информация контекста приложения делится между действиями в классе MyApp, который расширяет класс Application.

Я пишу unit test для своей активности, и я хочу проверить, что, когда пользователь нажимает кнопку в действии, состояние приложения будет меняться. Что-то вроде этого:

@Override
public void onClick(View pView) {
    ((MyApp)getApplicationContext()).setNewState();
}   

Проблема в том, что я не знаю, как издеваться над этим контекстом приложения. Я использую ActivityUnitTestCase в качестве базы тестового примера. Когда я вызываю setApplication, он изменяет значение члена mApplication класса Activity, но не контекст приложения. Я также попробовал setActivityContext, но он кажется неправильным (это не контекст приложения, а контекст активности), и он запускает assert внутри startActivity).

Итак, вопрос в том, как сделать mock getApplicationContext()?

Ответы

Ответ 1

Так как метод getApplicationContext находится внутри класса, который вы расширяете, он становится несколько проблематичным. Есть несколько проблем, которые следует учитывать:

  • Вы действительно не можете издеваться над классом, который находится под тестированием, что является одним из многих недостатков с наследованием объектов (например, подклассификация).
  • Другая проблема заключается в том, что ApplicationContext является singleton, что делает его еще более злым для тестирования, поскольку вы не можете легко высмеять глобальное состояние, которое запрограммировано как незаменимое.

Что вы можете сделать в этой ситуации, так это предпочесть структуру объекта над наследованием. Поэтому, чтобы сделать ваш Activity testable, вам нужно немного разделить логику. Допустим, что ваш Activity называется MyActivity. Он должен состоять из логического компонента (или класса), давайте назовите его MyActivityLogic. Вот простой рисунок диаграммы:

MyActivity and MyActivityLogic UML diagram from yUml

Чтобы решить проблему singleton, мы позволяем логике "вводить" контекст приложения, поэтому ее можно протестировать с помощью макета. Затем нам нужно только проверить, что объект MyActivity помещает корректный контекст приложения в MyActivityLogic. Как мы в основном решаем обе проблемы, через еще один уровень абстракции (перефразированный из Батлера Лампсона). Новый слой, который мы добавляем в этом случае, - это логика активности, перемещенная за пределы объекта активности.

Для вашего примера классы должны выглядеть примерно так:

public final class MyActivityLogic {

    private MyApp mMyApp;

    public MyActivityLogic(MyApp pMyApp) {
        mMyApp = pMyApp;
    }

    public MyApp getMyApp() {
        return mMyApp;
    }

    public void onClick(View pView) {
        getMyApp().setNewState();
    }
}

public final class MyActivity extends Activity {

    // The activity logic is in mLogic
    private final MyActivityLogic mLogic;

    // Logic is created in constructor
    public MyActivity() {
        super(); 
        mLogic = new MyActivityLogic(
            (MyApp) getApplicationContext());
    }

    // Getter, you could make a setter as well, but I leave
    // that as an exercise for you
    public MyActivityLogic getMyActivityLogic() {
        return mLogic;
    }

    // The method to be tested
    public void onClick(View pView) {
        mLogic.onClick(pView);
    }

    // Surely you have other code here...

}

Все должно выглядеть примерно так: classes with methods made in yUml

Для тестирования MyActivityLogic вам понадобится только простой jUnit TestCase вместо ActivityUnitTestCase (поскольку это не Activity), и вы можете издеваться над своим контекстом приложения, используя вашу насмешливую структуру выбора (с тех пор, как выполняется перенос ваши собственные макеты немного перетаскивают). В примере используется Mockito:

MyActivityLogic mLogic; // The CUT, Component Under Test
MyApplication mMyApplication; // Will be mocked

protected void setUp() {
    // Create the mock using mockito.
      mMyApplication = mock(MyApplication.class);
    // "Inject" the mock into the CUT
      mLogic = new MyActivityLogic(mMyApplication);
}

public void testOnClickShouldSetNewStateOnAppContext() {
    // Test composed of the three A        
    // ARRANGE: Most stuff is already done in setUp

    // ACT: Do the test by calling the logic
    mLogic.onClick(null);

    // ASSERT: Make sure the application.setNewState is called
    verify(mMyApplication).setNewState();
}

Чтобы протестировать MyActivity, вы используете ActivityUnitTestCase, как обычно, нам нужно только убедиться, что он создает MyActivityLogic с правильным ApplicationContext. Пример скриншотного тестового кода, который делает все это:

// ARRANGE:
MyActivity vMyActivity = getActivity();
MyApp expectedAppContext = vMyActivity.getApplicationContext();

// ACT: 
// No need to "act" much since MyActivityLogic object is created in the 
// constructor of the activity
MyActivityLogic vLogic = vMyActivity.getMyActivityLogic();

// ASSERT: Make sure the same ApplicationContext singleton is inside
// the MyActivityLogic object
MyApp actualAppContext = vLogic.getMyApp();
assertSame(expectedAppContext, actualAppContext);

Надеюсь, что все это имеет смысл для вас и поможет вам.