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で 。
おそらくこれは、テストモジュールのオーバーライドを適切にサポートする回避策ですが、実稼働モジュールをテストモジュールでオーバーライドできます。以下のコードスニペットは、コンポーネントとモジュールが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
クラスに設定する必要があります-StringHolder
がonCreate
コールバックに挿入されるためです。
Dagger v2.0.0では、コンポーネントは他のインターフェイスを拡張できます。これを利用して、extendsTestAppComponent
であるAppComponent
を作成できます。
@Component(modules = TestStringHolderModule.class)
interface TestAppComponent extends AppComponent {
}
これで、テストモジュールを定義できるようになりました。 TestStringHolderModule
。最後のステップは、以前に追加されたApp
クラスのセッターメソッドを使用してテストコンポーネントを設定することです。これは、アクティビティを作成する前に行うことが重要です。
((App) application).setTestComponent(mTestAppComponent);
エスプレッソ
Espressoでは、アクティビティが作成される前にコンポーネントを交換できるカスタムActivityTestRule
を作成しました。 DaggerActivityTestRule
here のコードを見つけることができます。
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(...)
}
}
@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);
@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()
メソッドをオーバーライドして、テストモジュールでテストコンポーネントを設定します。 MyTestComponent
がMyComponent
の子孫である場合にのみ機能することに注意してください。
_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を使用している場合は、実行メニューから[構成の編集]をクリックし、[特定のインスツルメンテーションランナー]にテストランナーの名前を入力する必要があります。
以上です!この情報が誰かの助けになることを願っています:)
私は別の方法を見つけたようで、今のところうまくいっています。
まず、コンポーネント自体ではないコンポーネントインターフェイス:
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;
}
}
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.myServerEndpoint()
つまり、「モックモジュール」を拡張して元のモジュールを置き換えることはできません。いいえ、そう簡単ではありません。また、クラスごとにモジュールを直接バインドするようにコンポーネントを設計することを検討すると、「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の答えを参照してください。
あなたは私のソリューションをチェックアウトできますか、私はサブコンポーネントの例を含めました: https://github.com/nongdenchet/Android-mvvm-with-tests 。 @vaughandroidに感謝します。私はあなたのオーバーライド方法を借りました。主なポイントは次のとおりです。
クラスを作成してサブコンポーネントを作成します。私のカスタムアプリケーションは、このクラスのインスタンスも保持します。
// 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);
...
}
}
上記のMyApplicationクラスを拡張するカスタムTestApplicationがあります。このクラスには、ルートコンポーネントとビルダーを置き換える2つのメソッドが含まれています。
public class TestApplication extends MyApplication {
public void setComponent(AppComponent appComponent) {
this.mAppComponent = appComponent;
}
public void setComponentBuilder(ComponentBuilder componentBuilder) {
this.mComponentBuilder = componentBuilder;
}
}
最後に、モジュールとビルダーの依存関係をモックまたはスタブして、アクティビティに偽の依存関係を提供します。
@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());
}
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);
....
}