web-dev-qa-db-ja.com

Dagger 2.0の単体テストでモジュール/依存関係をオーバーライドするにはどうすればよいですか?

simple Android activity 単一の依存関係があります。この依存関係をアクティビティのonCreateに挿入します:

Dagger_HelloComponent.builder()
    .helloModule(new HelloModule(this))
    .build()
    .initialize(this);

私のActivityUnitTestCaseでは、Mockitoモックで依存関係をオーバーライドしたいです。モックを提供するテスト固有のモジュールを使用する必要があると思いますが、このモジュールをオブジェクトグラフに追加する方法がわかりません。

Dagger 1.xでは、これは明らかに このようなもの で行われます。

@Before
public void setUp() {
  ObjectGraph.create(new TestModule()).inject(this);
}

上記と同等のDagger 2.0とは何ですか?

私のプロジェクトとその単体テストを見ることができます GitHubで

54
G. Lombard

おそらくこれは、テストモジュールのオーバーライドを適切にサポートする回避策ですが、実稼働モジュールをテストモジュールでオーバーライドできます。以下のコードスニペットは、コンポーネントとモジュールが1つだけの単純なケースを示していますが、これはどのシナリオでも機能するはずです。多くの定型文とコードの繰り返しが必要なので、これに注意してください。将来、これを達成するためのより良い方法があると確信しています。

EspressoとRobolectricの例を含むプロジェクト も作成しました。この回答は、プロジェクトに含まれるコードに基づいています。

ソリューションには2つのことが必要です。

  • @Componentの追加のセッターを提供します
  • テストコンポーネントは、運用コンポーネントを拡張する必要があります

以下のような単純なApplicationがあると仮定します。

public class App extends Application {

    private AppComponent mAppComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        mAppComponent = DaggerApp_AppComponent.create();
    }

    public AppComponent component() {
        return mAppComponent;
    }

    @Singleton
    @Component(modules = StringHolderModule.class)
    public interface AppComponent {

        void inject(MainActivity activity);
    }

    @Module
    public static class StringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder("Release string");
        }
    }
}

Appクラスにメソッドを追加する必要があります。これにより、実稼働コンポーネントを置き換えることができます。

/**
 * Visible only for testing purposes.
 */
// @VisibleForTesting
public void setTestComponent(AppComponent appComponent) {
    mAppComponent = appComponent;
}

ご覧のとおり、StringHolderオブジェクトには「リリース文字列」値が含まれています。このオブジェクトはMainActivityに注入されます。

public class MainActivity extends ActionBarActivity {

    @Inject
    StringHolder mStringHolder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ((App) getApplication()).component().inject(this);
    }
}

テストでは、StringHolderに「テスト文字列」を指定します。 Appが作成される前に、テストコンポーネントをMainActivityクラスに設定する必要があります-StringHolderonCreateコールバックに挿入されるためです。

Dagger v2.0.0では、コンポーネントは他のインターフェイスを拡張できます。これを利用して、extendsTestAppComponentであるAppComponentを作成できます。

@Component(modules = TestStringHolderModule.class)
interface TestAppComponent extends AppComponent {

}

これで、テストモジュールを定義できるようになりました。 TestStringHolderModule。最後のステップは、以前に追加されたAppクラスのセッターメソッドを使用してテストコンポーネントを設定することです。これは、アクティビティを作成する前に行うことが重要です。

((App) application).setTestComponent(mTestAppComponent);

エスプレッソ

Espressoでは、アクティビティが作成される前にコンポーネントを交換できるカスタムActivityTestRuleを作成しました。 DaggerActivityTestRulehere のコードを見つけることができます。

Espressoを使用したサンプルテスト:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityEspressoTest {

    public static final String TEST_STRING = "Test string";

    private TestAppComponent mTestAppComponent;

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule =
            new DaggerActivityTestRule<>(MainActivity.class, new OnBeforeActivityLaunchedListener<MainActivity>() {
                @Override
                public void beforeActivityLaunched(@NonNull Application application, @NonNull MainActivity activity) {
                    mTestAppComponent = DaggerMainActivityEspressoTest_TestAppComponent.create();
                    ((App) application).setTestComponent(mTestAppComponent);
                }
            });

    @Component(modules = TestStringHolderModule.class)
    interface TestAppComponent extends AppComponent {

    }

    @Module
    static class TestStringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder(TEST_STRING);
        }
    }

    @Test
    public void checkSomething() {
        // given
        ...

        // when
        onView(...)

        // then
        onView(...)
                .check(...);
    }
}

ロボエレクトリック

RuntimeEnvironment.applicationのおかげで、Robolectricの方がはるかに簡単です。

Robolectricを使用したサンプルテスト:

@RunWith(RobolectricGradleTestRunner.class)
@Config(emulateSdk = 21, reportSdk = 21, constants = BuildConfig.class)
public class MainActivityRobolectricTest {

    public static final String TEST_STRING = "Test string";

    @Before
    public void setTestComponent() {
        AppComponent appComponent = DaggerMainActivityRobolectricTest_TestAppComponent.create();
        ((App) RuntimeEnvironment.application).setTestComponent(appComponent);
    }

    @Component(modules = TestStringHolderModule.class)
    interface TestAppComponent extends AppComponent {

    }

    @Module
    static class TestStringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder(TEST_STRING);
        }
    }

    @Test
    public void checkSomething() {
        // given
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

        // when
        ...

        // then
        assertThat(...)
    }
}
46
tomrozb

@EpicPandaForceが正しく言っているように、モジュールを拡張することはできません。しかし、私はこのための卑劣な回避策を思い付きました。他の例が苦しんでいる定型文の多くを避けることができると思います。

モジュールを「拡張」するための秘Theは、部分的なモックを作成し、オーバーライドするプロバイダーメソッドをモックアウトすることです。

Mockito を使用:

MyModule module = Mockito.spy(new MyModule());
Mockito.doReturn("mocked string").when(module).provideString();

MyComponent component = DaggerMyComponent.builder()
        .myModule(module)
        .build();

app.setComponent(component);

this Gist を作成して、完全な例を示します。

[〜#〜] edit [〜#〜]

次のように、部分的なモックがなくてもこれを行うことができます:

MyComponent component = DaggerMyComponent.builder()
        .myModule(new MyModule() {
            @Override public String provideString() {
                return "mocked string";
            }
        })
        .build();

app.setComponent(component);
24
vaughandroid

@tomrozbによって提案された回避策は非常に優れており、私を正しい軌道に乗せましたが、私の問題はPRODUCTION ApplicationクラスのsetTestComponent()メソッドを公開したことでした。実稼働アプリケーションがテスト環境について何も知る必要がないように、これをわずかに異なる方法で動作させることができました。

TL; DR-テストコンポーネントとモジュールを使用するテストアプリケーションでApplicationクラスを拡張します。次に、運用アプリケーションではなくテストアプリケーションで実行するカスタムテストランナーを作成します。


編集:このメソッドは、グローバルな依存関係(通常は_@Singleton_でマークされている)に対してのみ機能します。アプリに異なるスコープ(アクティビティごとなど)のコンポーネントがある場合は、スコープごとにサブクラスを作成するか、@ tomrozbの元の回答を使用する必要があります。これを指摘してくれた@tomrozbに感謝します!


この例では AndroidJUnitRunner テストランナーを使用していますが、これはおそらく Robolectric などに適用できます。

まず、本番アプリケーション。次のようになります。

_public class MyApp extends Application {
    protected MyComponent component;

    public void setComponent() {
        component = DaggerMyComponent.builder()
                .myModule(new MyModule())
                .build();
        component.inject(this);
    }

    public MyComponent getComponent() {
        return component;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        setComponent();
    }
}
_

このように、私のアクティビティと_@Inject_を使用する他のクラスは、単にgetApp().getComponent().inject(this);のようなものを呼び出して、依存関係グラフに自分自身を注入する必要があります。

完全を期すために、ここに私のコンポーネントがあります:

_@Singleton
@Component(modules = {MyModule.class})
public interface MyComponent {
    void inject(MyApp app);
    // other injects and getters
}
_

そして私のモジュール:

_@Module
public class MyModule {
    // EDIT: This solution only works for global dependencies
    @Provides @Singleton
    public MyClass provideMyClass() { ... }

    // ... other providers
}
_

テスト環境では、テストコンポーネントを運用コンポーネントから拡張します。これは、@ tomrozbの回答と同じです。

_@Singleton
@Component(modules = {MyTestModule.class})
public interface MyTestComponent extends MyComponent {
    // more component methods if necessary
}
_

また、テストモジュールは何でも構いません。おそらくあなたはここであなたのモックやものを扱うでしょう(私はMockitoを使用しています)。

_@Module
public class MyTestModule {
    // EDIT: This solution only works for global dependencies
    @Provides @Singleton
    public MyClass provideMyClass() { ... }

    // Make sure to implement all the same methods here that are in MyModule, 
    // even though it's not an override.
}
_

だから今、トリッキーな部分。本番アプリケーションクラスから拡張するテストアプリケーションクラスを作成し、setComponent()メソッドをオーバーライドして、テストモジュールでテストコンポーネントを設定します。 MyTestComponentMyComponentの子孫である場合にのみ機能することに注意してください。

_public class MyTestApp extends MyApp {

    // Make sure to call this method during setup of your tests!
    @Override
    public void setComponent() {
        component = DaggerMyTestComponent.builder()
                .myTestModule(new MyTestModule())
                .build();
        component.inject(this)
    }
}
_

テストを開始する前に、アプリでsetComponent()を呼び出して、グラフが正しく設定されていることを確認してください。このようなもの:

_@Before
public void setUp() {
    MyTestApp app = (MyTestApp) getInstrumentation().getTargetContext().getApplicationContext();
    app.setComponent()
    ((MyTestComponent) app.getComponent()).inject(this)
}
_

最後に、最後に欠けているのは、TestRunnerをカスタムテストランナーでオーバーライドすることです。私のプロジェクトではAndroidJUnitRunnerを使用していましたが、できるように見えます Robolectricでも同じことをします

_public class TestRunner extends AndroidJUnitRunner {
    @Override
    public Application newApplication(@NonNull ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        return super.newApplication(cl, MyTestApp.class.getName(), context);
    }
}
_

また、次のようにtestInstrumentationRunner gradleを更新する必要があります。

_testInstrumentationRunner "com.mypackage.TestRunner"
_

また、Android Studioを使用している場合は、実行メニューから[構成の編集]をクリックし、[特定のインスツルメンテーションランナー]にテストランナーの名前を入力する必要があります。

以上です!この情報が誰かの助けになることを願っています:)

9
yuval

私は別の方法を見つけたようで、今のところうまくいっています。

まず、コンポーネント自体ではないコンポーネントインターフェイス:

MyComponent.Java

interface MyComponent {
    Foo provideFoo();
}

次に、2つの異なるモジュールがあります。実際のモジュールとテストモジュールです。

MyModule.Java

@Module
class MyModule {
    @Provides
    public Foo getFoo() {
        return new Foo();
    }
}

TestModule.Java

@Module
class TestModule {
    private Foo foo;
    public void setFoo(Foo foo) {
        this.foo = foo;
    }

    @Provides
    public Foo getFoo() {
        return foo;
    }
}

そして、これら2つのモジュールを使用する2つのコンポーネントがあります。

MyRealComponent.Java

@Component(modules=MyModule.class)
interface MyRealComponent extends MyComponent {
    Foo provideFoo(); // without this dagger will not do its magic
}

MyTestComponent.Java

@Component(modules=TestModule.class)
interface MyTestComponent extends MyComponent {
    Foo provideFoo();
}

アプリケーションではこれを行います:

MyComponent component = DaggerMyRealComponent.create();
<...>
Foo foo = component.getFoo();

テストコードでは次を使用します。

TestModule testModule = new TestModule();
testModule.setFoo(someMockFoo);
MyComponent component = DaggerMyTestComponent.builder()
    .testModule(testModule).build();
<...>
Foo foo = component.getFoo(); // will return someMockFoo

問題は、MyModuleのすべてのメソッドをTestModuleにコピーする必要があることですが、外部から直接設定されない限り、MyModuleをTestModule内に持ち、MyModuleのメソッドを使用することで実行できます。このような:

TestModule.Java

@Module
class TestModule {
    MyModule myModule = new MyModule();
    private Foo foo = myModule.getFoo();
    public void setFoo(Foo foo) {
        this.foo = foo;
    }

    @Provides
    public Foo getFoo() {
        return foo;
    }
}
2
aragaer

THIS ANSWER IS OBSOLETE。READ BELOW EDIT.

残念なことに、モジュールから拡張できません、または次のコンパイルエラーが発生します:

Error:(24, 21) error: @Provides methods may not override another method.
Overrides: Provides 
    retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.mySe‌​rverEndpoint()

つまり、「モックモジュール」を拡張して元のモジュールを置き換えることはできません。いいえ、そう簡単ではありません。また、クラスごとにモジュールを直接バインドするようにコンポーネントを設計することを検討すると、「TestComponent」を実際に作成することもできません。これは、すべてゼロから、すべてのバリエーションのコンポーネントを作成する必要があります!明らかにそれはオプションではありません。

小規模では、私がやったことは、モジュールに与える「プロバイダー」を作成することです。これは、モックまたはプロダクションタイプを選択するかどうかを決定します。

public interface EndpointProvider {
    Endpoint serverEndpoint();
}

public class ProdEndpointProvider implements EndpointProvider {

    @Override
    public Endpoint serverEndpoint() {
        return new ServerEndpoint();
    }
}


public class TestEndpointProvider implements EndpointProvider {
    @Override
    public Endpoint serverEndpoint() {
        return new TestServerEndpoint();
    }
}

@Module
public class EndpointModule {
    private Endpoint serverEndpoint;

    private EndpointProvider endpointProvider;

    public EndpointModule(EndpointProvider endpointProvider) {
        this.endpointProvider = endpointProvider;
    }

    @Named("server")
    @Provides
    public Endpoint serverEndpoint() {
        return endpointProvider.serverEndpoint();
    }
}

編集:どうやら、エラーメッセージが言うように、@Providesアノテーション付きメソッドを使用して別のメソッドをオーバーライドすることはできませんが、それは@Providesアノテーション付きメソッドをオーバーライドできないという意味ではありません:(

その魔法はすべて無用でした!メソッドに@Providesを付けずにモジュールを拡張するだけで機能します... @vaughandroidの答えを参照してください。

1
EpicPandaForce

あなたは私のソリューションをチェックアウトできますか、私はサブコンポーネントの例を含めました: https://github.com/nongdenchet/Android-mvvm-with-tests 。 @vaughandroidに感謝します。私はあなたのオーバーライド方法を借りました。主なポイントは次のとおりです。

  1. クラスを作成してサブコンポーネントを作成します。私のカスタムアプリケーションは、このクラスのインスタンスも保持します。

    // The builder class
    public class ComponentBuilder {
     private AppComponent appComponent;
    
     public ComponentBuilder(AppComponent appComponent) {
      this.appComponent = appComponent;
     }
    
     public PlacesComponent placesComponent() {
      return appComponent.plus(new PlacesModule());
     }
    
     public PurchaseComponent purchaseComponent() {
      return appComponent.plus(new PurchaseModule());
     }
    }
    
    // My custom application class
    public class MyApplication extends Application {
    
     protected AppComponent mAppComponent;
     protected ComponentBuilder mComponentBuilder;
    
     @Override
     public void onCreate() {
      super.onCreate();
    
      // Create app component
      mAppComponent = DaggerAppComponent.builder()
              .appModule(new AppModule())
              .build();
    
      // Create component builder
      mComponentBuilder = new ComponentBuilder(mAppComponent);
     }
    
     public AppComponent component() {
      return mAppComponent;
     }
    
     public ComponentBuilder builder() {
      return mComponentBuilder;
     } 
    }
    
    // Sample using builder class:
    public class PurchaseActivity extends BaseActivity {
     ...    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
      ...
      // Setup dependency
      ((MyApplication) getApplication())
              .builder()
              .purchaseComponent()
              .inject(this);
      ...
     }
    }
    
  2. 上記のMyApplicationクラスを拡張するカスタムTestApplicationがあります。このクラスには、ルートコンポーネントとビルダーを置き換える2つのメソッドが含まれています。

    public class TestApplication extends MyApplication {
     public void setComponent(AppComponent appComponent) {
      this.mAppComponent = appComponent;
     }
    
     public void setComponentBuilder(ComponentBuilder componentBuilder) {
      this.mComponentBuilder = componentBuilder;
     }
    }    
    
  3. 最後に、モジュールとビルダーの依存関係をモックまたはスタブして、アクティビティに偽の依存関係を提供します。

    @MediumTest
    @RunWith(AndroidJUnit4.class)
    public class PurchaseActivityTest {
    
     @Rule
     public ActivityTestRule<PurchaseActivity> activityTestRule =
         new ActivityTestRule<>(PurchaseActivity.class, true, false);
    
     @Before
     public void setUp() throws Exception {
     PurchaseModule stubModule = new PurchaseModule() {
         @Provides
         @ViewScope
         public IPurchaseViewModel providePurchaseViewModel(IPurchaseApi purchaseApi) {
             return new StubPurchaseViewModel();
         }
     };
    
     // Setup test component
     AppComponent component = ApplicationUtils.application().component();
     ApplicationUtils.application().setComponentBuilder(new ComponentBuilder(component) {
         @Override
         public PurchaseComponent purchaseComponent() {
             return component.plus(stubModule);
         }
     });
    
     // Run the activity
     activityTestRule.launchActivity(new Intent());
    }
    
0
Rain Vu

Robolectric 3。+の解決策があります。

私は作成時に注入せずにテストしたいMainActivityを持っています:

public class MainActivity extends BaseActivity{

  @Inject
  public Configuration configuration;

  @Inject
  public AppStateService appStateService;

  @Inject
  public LoginService loginService;

  @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.processIntent(getIntent()); // this is point where pass info from test
      super.onCreate(savedInstanceState)
    ...
  }
  ...
 }

次に、BaseActivity:

public class BaseActivity extends AppCompatActivity {

  protected Logger mLog;

  protected boolean isTestingSession = false; //info about test session


  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
      if (!isTestingSession) { // check if it is in test session, if not enable injectig
          AndroidInjection.inject(this);
      }
      super.onCreate(savedInstanceState);
  }

  // method for receive intent from child and scaning if has item TESTING with true
  protected void processIntent(Intent intent) {
    if (intent != null && intent.getExtras() != null) {
        isTestingSession = intent.getExtras().getBoolean("TESTING", false);
    }
  }

最後に私のテストクラス:

@Before
public void setUp() throws Exception {
  ...
  // init mocks...
   loginServiceMock = mock(LoginService.class);
   locServiceMock = mock(LocationClientService.class);
   fakeConfiguration = new ConfigurationUtils(new ConfigurationXmlParser());
   fakeConfiguration.save(FAKE_XML_CONFIGURATION);
   appStateService = new AppStateService(fakeConfiguration, locServiceMock, RuntimeEnvironment.application);

   // prepare activity
   Intent intent = new Intent(RuntimeEnvironment.application, MainActivity.class);
   intent.putExtra("TESTING", true);
   ActivityController<MainActivity> activityController = Robolectric.buildActivity(MainActivity.class, intent); // place to put bundle with extras

    // get the activity instance
    mainActivity = activityController.get();


    // init fields which should be injected
    mainActivity.appStateService = appStateService;
    mainActivity.loginService = loginServiceMock;
    mainActivity.configuration = fakeConfiguration;


    // and whoala 
    // now setup your activity after mock injection
    activityController.setup();

    // get views etc..
    actionButton = mainActivity.findViewById(R.id.mainButtonAction);
    NavigationView navigationView = mainActivity.findViewById(R.id.nav_view);

  ....
  }
0
Andrew Sneck