web-dev-qa-db-ja.com

AndroidのEspresso機能テストを実行するときに、Daggerにモックオブジェクトを挿入させる

DIの概念が完全に理にかなっているので、私は最近Daggerに夢中になりました。 DIの優れた「副産物」の1つ(Jake Whartonが彼のプレゼンテーションの1つに書いたように)は、テストのしやすさです。

そのため、現在は基本的にEspressoを使用して機能テストを行っており、ダミー/モックデータをアプリケーションに挿入して、アクティビティにそれらを表示できるようにしたいと考えています。これはDIの最大の利点の1つであるため、これは比較的簡単な質問であると思います。どういうわけか、頭を包み込むことができないようです。どんな助けでも大歓迎です。これが私がこれまでに持っているものです(私は私の現在の設定を反映する例を書きました):

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"
        }
    }
}

これはまったく機能していません。私のEspressoテストは実行されますが、TestNavigationModuleは完全に無視されます... arr ... :(

私は何が間違っているのですか? Espressoでモジュールをモックアウトするためのより良いアプローチはありますか? Robolectric、Mockitoなどが使用されている例を検索して見ました。しかし、私は純粋なEspressoテストが必要であり、モジュールを私の模擬モジュールと交換する必要があります。これをどのように行うべきですか?

編集:

そこで、静的テストモジュールリスト定義を作成し、nullをチェックして、それをApplicationクラスに追加する@ user3399328アプローチを採用しました。それでも、テストで注入されたバージョンのクラスを取得できません。エスプレッソのライフサイクルではなく、短剣のテストモジュールの定義に問題があるのではないかと感じています。私が仮定している理由は、デバッグステートメントを追加したところ、アプリケーションクラスへの注入時に静的テストモジュールが空ではないことがわかったためです。私が間違っている可能性のある方向を教えていただけませんか。これが私の定義のコードスニペットです:

私のアプリケーション:

@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;
    }
}
17
Kaushik Gopal

アプローチは1回しか発生しないため機能しません。マットが述べたように、アクティビティの実際のインジェクションコードを実行すると、特別なオブジェクトグラフによってインジェクションされた変数がすべて消去されます。

これを機能させるには2つの方法があります。

簡単な方法:アクティビティにパブリック静的変数を作成して、テストでオーバーライドモジュールを割り当て、nullでない場合は実際のアクティビティコードに常にこのモジュールを含めることができます(これはテストでのみ発生します)。それは私の答えに似ています ここ アプリケーションではなくアクティビティの基本クラスのためだけに。

より長く、おそらくより良い方法:すべてのアクティビティインジェクション(そしてより重要なのはグラフの作成)がActivityInjectHelperのような1つのクラスで行われるようにコードをリファクタリングします。テストパッケージで、ActivityInjectHelperという名前の別のクラスをexact sameパッケージパスで作成します。このクラスは、テストモジュールにプラスすることを除いて、同じメソッドを実装します。テストクラスが最初にロードされるため、アプリケーションはテストActivityInjectHelperで実行されます。繰り返しますが、それは私の答えに似ています ここ 別のクラスのためだけに。

更新:

あなたがより多くのコードを投稿し、それはほぼ機能しているようですが、葉巻はありません。アクティビティとアプリケーションの両方で、onCreate()を実行する前にテストモジュールをスナックインする必要があります。アクティビティオブジェクトグラフを処理する場合、テストのgetActivity()の前であればいつでも問題ありません。アプリケーションを扱う場合、setUp()の実行時までにonCreate()がすでに呼び出されているため、少し難しくなります。幸いなことに、テストのコンストラクターでそれを行うことは機能します-アプリケーションはその時点では作成されていません。これについては、最初のリンクで簡単に説明します。

8
user3399328

Dagger2とEspresso2により、状況は確かに改善されました。これは、テストケースが現在どのように見えるかを示しています。 ContributorsModelがDaggerによって提供されていることに注意してください。ここで利用可能な完全なデモ: 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"))));
}
10
pmellaaho

GetActivityを呼び出すと、実際にはプロセスでonCreateを呼び出すアクティビティが開始されます。つまり、テストモジュールがグラフに追加されて使用されることはありません。 activityInstrumentationTestcase2を使用すると、アクティビティスコープに適切に注入することはできません。アプリケーションを使用してアクティビティへの依存関係を提供し、アクティビティが使用するモックオブジェクトをそれに注入することで、これを回避しました。理想的ではありませんが、機能します。 Ottoなどのイベントバスを使用して、依存関係を提供できます。

1
Matt Wolfe

編集:以下の投稿形式 http://systemdotrun.blogspot.co.uk/2014/11/Android-testing-with-dagger-retrofit.html

Espresso + Daggerを使用してActivityをテストするには、以下を実行しました

@ user3399328からの回答に触発されて、Applicationクラス内にDaggerHelperクラスがあります。これにより、テストケースはモックを提供するTest _@Provider_を使用して_@Modules_ sをオーバーライドできます。限り

1)これは、testCases getActivity()呼び出しが行われる前に行われます(注入呼び出しは_Activity.onCreate_内のアクティビティで発生するため)

2)tearDownは、オブジェクトグラフからテストモジュールを削除します。

以下の例。

注:これはIoCのファクトリメソッドを使用する場合と同様の落とし穴があるため理想的ではありませんが、少なくともこの方法では、tearDown()を1回呼び出すだけで、システムをテスト対象に戻すことができます。通常に。

私の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();
}
_
0
Dori