In my test
#LargeTest
#RunWith(AndroidJUnit4.class)
public class SimpleActicityTest {
#Rule
public ActivityTestRule<MainActivity> activityActivityTestRule =
new ActivityTestRule<>(MainActivity.class);
#Test
public void testActivity() throws Exception {
Activity a = activityActivityTestRule.getActivity();
Screenshot.snapActivity(activity).setName("s1").record();
}
I have
ava.lang.NullPointerException: Attempt to invoke virtual method 'android.content.Context android.app.Instrumentation.getContext()' on a null object reference
at com.facebook.testing.screenshot.internal.ScreenshotImpl.getInstance(ScreenshotImpl.java:338)
at com.facebook.testing.screenshot.Screenshot.snapActivity(Screenshot.java:45)
at com.mobium.reference.activity.Util.takeScreenshot(Util.java:32)
at com.mobium.reference.activity.SimpleActicityTest.lambda$testLeftMenuTest$1(SimpleActicityTest.java:52)
at com.mobium.reference.activity.SimpleActicityTest.access$lambda$0(SimpleActicityTest.java)
at com.mobium.reference.activity.SimpleActicityTest$$Lambda$1.run(Unknown Source)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:194)
at android.app.ActivityThread.main(ActivityThread.java:5549)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:964)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:759)
source of faceebook's lib getInstance():
public static ScreenshotImpl getInstance() {
if(sInstance != null) {
return sInstance;
} else {
Class var0 = ScreenshotImpl.class;
synchronized(ScreenshotImpl.class) {
if(sInstance != null) {
return sInstance;
} else {
Instrumentation instrumentation = Registry.getRegistry().instrumentation;
Bundle arguments = Registry.getRegistry().arguments;
HostFileSender hostFileSender = new HostFileSender(instrumentation, arguments);
sInstance = create(instrumentation.getContext(), arguments, hostFileSender);
return sInstance;
}
}
}
}
It creates Registry and take public field instrumentation, but there are not instrumentation initializations in Registry constructor. How it can work?
If you want to use AndroidJUnit4 you have to create a custom test runner where the screenshot stuff is initialized.
As described here:
https://facebook.github.io/screenshot-tests-for-android/#custom-test-runner
Create a custom test runner in the test directory (androidTest/java/...):
public class MyTestRunner extends AndroidJUnitRunner {
#Override
public void onCreate(Bundle args) {
ScreenshotRunner.onCreate(this, args);
super.onCreate(args);
}
#Override
public void finish(int resultCode, Bundle results) {
ScreenshotRunner.onDestroy();
super.finish(resultCode, results);
}
}
In the build.gradle set the testInstrumentationRunner to your custom runner:
defaultConfig {
// ...
testInstrumentationRunner "my.package.MyTestRunner"
}
Related
I'm trying to implement MVVM with Data binding + Samsung Health SDK, I'm aware a little bit about the MVVM concept:
View: this is the UI (activity, fragment, etc)
View Model: this is the bridge between your view and your model
Model: this could be your data source (DB, network source, etc)
so if I have fragment:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="isLoading" type="local.team.dev.shealth.mvvm.SplashViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/cardview_light_background"
android:orientation="vertical">
<TextView
android:id="#+id/loading_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical|center_horizontal"
android:text="#string/loading_splash"
android:textAlignment="center"
app:visibleGone="#{isLoading}"/>
</LinearLayout>
</layout>
and below is Samsung Health SDK works:
it has a service object and Datastore object as follow:
// where should i put this 2 objects in MVVM architecture?
HealthDataService healthDataService = new HealthDataService();
try {
healthDataService.initialize(this);
} catch (Exception e) {
e.printStackTrace();
}
// Create a HealthDataStore instance and set its listener
mStore = new HealthDataStore(this, mConnectionListener);
// Request the connection to the health data store
mStore.connectService();
which mStore using listener as follow to determine the connection status:
// where should i put this listener in MVVM architecture?
// and how to bind it with databinding in View UI
private final HealthDataStore.ConnectionListener mConnectionListener = new HealthDataStore.ConnectionListener() {
#Override
public void onConnected() {
Log.d(APP_TAG, "Health data service is connected.");
mReporter = new StepCountReporter(mStore);
if (isPermissionAcquired()) {
// if it is connected and permission acquired then go to next activity
} else {
requestPermission(); // <-- UI trigger data permission here
}
}
#Override
public void onConnectionFailed(HealthConnectionErrorResult error) {
Log.d(APP_TAG, "Health data service is not available.");
showConnectionFailureDialog(error);
// Can i display message to fragment ui // Can i display message to fragment ui
// using data binding if connection failed (and perhaps show retry button)
}
#Override
public void onDisconnected() {
Log.d(APP_TAG, "Health data service is disconnected.");
// Can i display message to fragment ui
// and tell the status with data binding
if (!isFinishing()) {
mStore.connectService();
}
}
};
the questions here is:
where should i put or expose healthDataService and HealthDataStore object? in app class? ViewModel? or Model?
healthDataService and HealthDataStore should survive when the app move to another activity to be able to use to collect data from Samsung Health
how to leverage the listener to be able to 'connect' with databinding and show the message status in Fragment UI TextView? where should i put the listener? in View model or in View?
there maybe no right or wrong answer here, but eventually, i'm looking after what is the right approach to do this...
thanks!
EDIT: below is what i'm trying to approach, however, it giving me null Object Reference T_T
Beware of the long content
I post this for a discussion of my approach, but it's not work yet.
In general this is how i do it:
+--------+ +-------------+ +----------------+ +----------+ +-------+
|activity+----+ActivityClass| |ApplicationClass+--+Repository+--+Service|
+--------+ +------+------+ +-----+----------+ +-----+----+ +-------+
| | |
+--------+ +------+------+ +-----+----+ +----+---+
|fragment+----+FragmentClass+----+View Model| |Listener|
|dataBind| |with DataBind| |Class | +--------+
+--------+ +-------------+ +----------+
so here is my app Class
class SHealth extends Application {
public static final String APP_TAG = "SimpleHealth";
private HealthDataStore mStore;
public HealthDataService getHealthService() { return HealthService.getInstance(); }
public HealthRepository getRepository() { return HealthRepository.getInstance(getHealthService(), this); }
}
and in my Activity and Fragment class i have it like this:
public class SplashActivity extends AppCompatActivity {
#Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
if (savedInstanceState == null) {
SplashFragment fragment = new SplashFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, fragment, SplashFragment.TAG).commit();
}
}
}
// MY FRAGMENT CLASS
public class SplashFragment extends Fragment {
public static final String TAG = "SplashViewModel";
private SHealth app;
private TextView loading_tv;
private FragmentSplashBinding mBinding;
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container,
#Nullable Bundle savedInstanceState) {
mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_splash, container, false);
return mBinding.getRoot();
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
app = (SHealth) this.getActivity().getApplication();
loading_tv = this.loading_tv;
SplashViewModel.Factory factory = new SplashViewModel.Factory(app);
// i'm not sure what's the diffrent using factory or not...
final SplashViewModel viewModel = ViewModelProviders.of(this, factory).get(SplashViewModel.class);
//final SplashViewModel viewModel = ViewModelProviders.of(this).get(SplashViewModel.class);
subscribeUi(viewModel);
}
private void subscribeUi(SplashViewModel viewModel) {
// Update the list when the data changes
LiveData<HealthDSConnectionListener.Status> mObservableStatus = viewModel.getConnectionStatus();
Observer<HealthDSConnectionListener.Status> observer = new Observer<HealthDSConnectionListener.Status>() {
#Override
public void onChanged(#Nullable HealthDSConnectionListener.Status status) {
if (status == HealthDSConnectionListener.Status.CONNECTED) {
Log.v("statusOnChange", "Connected");
//mBinding.setIsLoading(true);
//mBinding.setConnectionStatus("Connected");
} else if(status == HealthDSConnectionListener.Status.DISCONNECTED){
Log.v("statusOnChange", "DISCONNECTED");
//mBinding.setIsLoading(true);
//mBinding.setConnectionStatus("DISCONNECTED");
}else{
Log.v("statusOnChange", "Failed");
//mBinding.setIsLoading(true);
//mBinding.setConnectionStatus("Failed");
}
}
};
mObservableStatus.observe(this,observer);
}
}
and below is my ViewModel Class:
public class SplashViewModel extends AndroidViewModel {
private final MediatorLiveData<HealthDSConnectionListener.Status> mObservableStatus;
private final HealthDSConnectionListener mConnectionListener;
public SplashViewModel(#NonNull Application application) {
super(application);
HealthRepository repository = ((SHealth) application).getRepository();
mObservableStatus = new MediatorLiveData<>();
mObservableStatus.setValue(null);
mConnectionListener = new HealthDSConnectionListener(repository){
#Override
public void onConnected() {
super.onConnected();
Log.v("statusOnChange", "C");
}
#Override
public void onConnectionFailed(HealthConnectionErrorResult error) {
super.onConnectionFailed(error);
Log.v("statusOnChange", "F");
}
#Override
public void onDisconnected() {
super.onDisconnected();
Log.v("statusOnChange", "D");
}
};
repository.connectDataStore(mConnectionListener);
LiveData<HealthDSConnectionListener.Status> status = repository.getConnectionStatus();
mObservableStatus.addSource(status, mObservableStatus::setValue);
}
public LiveData<HealthDSConnectionListener.Status> getConnectionStatus(){ return mObservableStatus; }
public static class Factory extends ViewModelProvider.NewInstanceFactory {
#NonNull
private final Application mApplication;
private final HealthRepository mRepository;
public Factory(#NonNull Application application) {
mApplication = application;
mRepository = ((SHealth) application).getRepository();
}
#Override
public <T extends ViewModel> T create(Class<T> modelClass) {
//noinspection unchecked
return (T) new SplashViewModel(mApplication);
}
}
}
and below is my Repository Class:
public class HealthRepository {
private static HealthRepository sHealthInstance;
private final HealthDataService mService;
private HealthDataStore mStore;
private SHealth app;
private MutableLiveData<HealthDSConnectionListener.Status> DSConnectionStatus;
public MutableLiveData<HealthDSConnectionListener.Status> getDSConnectionStatus(){
if (DSConnectionStatus == null) { DSConnectionStatus = new MutableLiveData<HealthDSConnectionListener.Status>(); }
return DSConnectionStatus;
}
public void setDSConnectionStatus(HealthDSConnectionListener.Status status){ getDSConnectionStatus().setValue(status); }
private MediatorLiveData<HealthDSConnectionListener.Status> mObservableConnectionStatus;
private HealthRepository(final HealthDataService service, final SHealth context) {
this.app = context;
mService = service;
try { mService.initialize(app); }
catch (Exception e) { e.printStackTrace(); }
mObservableConnectionStatus = new MediatorLiveData<>();
mObservableConnectionStatus.setValue(null);
mObservableConnectionStatus.addSource(DSConnectionStatus, mObservableConnectionStatus::setValue);
}
public void connectDataStore(HealthDSConnectionListener listener) {
if(mStore == null) {
mStore = new HealthDataStore(app, listener);
mStore.connectService();
}
}
public static HealthRepository getInstance(final HealthDataService service, final Context context) {
if (sHealthInstance == null) {
synchronized (HealthRepository.class) {
if (sHealthInstance == null) {
sHealthInstance = new HealthRepository(service, (SHealth) context);
}
}
}
return sHealthInstance;
}
public LiveData<HealthDSConnectionListener.Status> getConnectionStatus() { return mObservableConnectionStatus; }
}
and here is my Service Class:
public abstract class HealthService {
private static HealthDataService sInstance;
public static HealthDataService getInstance() {
if (sInstance == null) {
synchronized (HealthService.class) {
if (sInstance == null) {
sInstance = new HealthDataService();
return sInstance;
}
}
}
return sInstance;
}
}
with my 'in-house' listener extending from the default API listener:
reason i'm extending default listener can be found here
public class HealthDSConnectionListener implements HealthDataStore.ConnectionListener {
public enum Status{
CONNECTED(1), DISCONNECTED(2), FAILED(0);
private final int index;
Status(int index) { this.index = index; }
int getIndex() { return this.index; }
}
private final HealthRepository repo;
public HealthDSConnectionListener(HealthRepository repo) { this.repo = repo; }
#Override public void onConnected() {
repo.setDSConnectionStatus(Status.CONNECTED);
}
#Override public void onDisconnected() {
repo.setDSConnectionStatus(Status.DISCONNECTED);
}
#Override public void onConnectionFailed(HealthConnectionErrorResult healthConnectionErrorResult) {
repo.setDSConnectionStatus(Status.FAILED);
}
}
But I have a problem with this approach, because it keep throw me:
I dont undrestand why it happen and how to solve it...
FATAL EXCEPTION: main
Process: local.team.dev.shealthdemo, PID: 3458
java.lang.RuntimeException: Unable to start activity ComponentInfo{local.team.dev.shealthdemo/local.team.dev.shealthdemo.SplashActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.arch.lifecycle.LiveData.observeForever(android.arch.lifecycle.Observer)' on a null object reference
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2416)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.arch.lifecycle.LiveData.observeForever(android.arch.lifecycle.Observer)' on a null object reference
at android.arch.lifecycle.MediatorLiveData$Source.plug(MediatorLiveData.java:141)
at android.arch.lifecycle.MediatorLiveData.onActive(MediatorLiveData.java:118)
at android.arch.lifecycle.LiveData$LifecycleBoundObserver.activeStateChanged(LiveData.java:389)
at android.arch.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged(LiveData.java:378)
at android.arch.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:353)
at android.arch.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:180)
at android.arch.lifecycle.LiveData.observe(LiveData.java:201)
at android.arch.lifecycle.LiveData.observeForever(LiveData.java:220)
at android.arch.lifecycle.MediatorLiveData$Source.plug(MediatorLiveData.java:141)
at android.arch.lifecycle.MediatorLiveData.onActive(MediatorLiveData.java:118)
at android.arch.lifecycle.LiveData$LifecycleBoundObserver.activeStateChanged(LiveData.java:389)
at android.arch.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged(LiveData.java:378)
at android.arch.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:353)
at android.arch.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:291)
at android.arch.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:331)
at android.arch.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:137)
at android.arch.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:123)
at android.support.v4.app.Fragment.performStart(Fragment.java:2391)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1458)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1740)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1809)
at android.support.v4.app.FragmentManagerImpl.dispatchStateChange(FragmentManager.java:3217)
at android.support.v4.app.FragmentManagerImpl.dispatchStart(FragmentManager.java:3176)
at android.support.v4.app.FragmentController.dispatchStart(FragmentController.java:203)
at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:570)
at android.support.v7.app.AppCompatActivity.onStart(AppCompatActivity.java:177)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1237)
at android.app.Activity.performStart(Activity.java:6253)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2379)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
I'm trying to write some tests for fragments which have fields annotated with #Inject. For example, a chunk of my app looks like this:
Module:
#Module
public class PdfFactoryModule {
#Provides #Singleton
PdfFactory providePdfFactory() {
return PdfFactory.getPdfFactory();
}
}
Component:
#Singleton
#Component(modules = PdfFactoryModule.class)
public interface CorePdfComponent {
void inject(PagerFragment pagerFragment);
}
Application:
public class CorePdfApplication extends Application {
#NonNull
private CorePdfComponent component;
#Override
public void onCreate() {
super.onCreate();
component = DaggerCorePdfComponent.builder().build();
}
#NonNull
public CorePdfComponent getComponent() {
return component;
}
}
PagerFragment:
public class PagerFragment extends Fragment {
#Inject PdfFactory pdfFactory;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Dagger 2
((CorePdfApplication) getActivity().getApplication()).getComponent().inject(this);
}
(Note that these are only snippets of my whole code, I'm showing only the essentials for this particular dependency to keep it clear.)
I was trying to do a test like this:
Fake Module:
#Module
public class FakePdfFactoryModule extends PdfFactoryModule {
#Override
PdfFactory providePdfFactory() {
return Mockito.mock(PdfFactory.class);
}
}
Fake Component:
#Singleton
#Component(modules = FakePdfFactoryModule.class)
public interface FakeCorePdfComponent extends CorePdfComponent {
void inject(PagerFragmentTest pagerFragmentTest);
}
Fake Application:
public class FakeCorePdfApplication extends CorePdfApplication {
#NonNull
private FakeCorePdfComponent component;
#Override
public void onCreate() {
super.onCreate();
component = DaggerFakeCorePdfComponent.builder().build();
}
#NonNull
public FakeCorePdfComponent getComponent() {
return component;
}
}
Test:
#RunWith(RobolectricTestRunner.class)
#Config(constants = BuildConfig.class, sdk = 21, application = FakeCorePdfApplication.class)
public class PagerFragmentTest {
PagerFragment pagerFragment;
#Before
public void setup() {
pagerFragment = new PagerFragment();
startVisibleFragment(pagerFragment);
}
#Test
public void exists() throws Exception {
assertNotNull(pagerFragment);
}
But the DaggerFakeCorePdfComponent doesn't generate. I may have messed up big time because I never tested with dependency injection. What am I doing wrong?
My advice - "Do not use dagger in tests".
Just change your code to next:
public class FakeCorePdfApplication extends CorePdfApplication {
#NonNull
private CorePdfComponent component = mock(CorePdfComponent.class);
#Override
public void onCreate() {
super.onCreate();
}
#NonNull
public CorePdfComponent getComponent() {
return component;
}
}
And:
#RunWith(RobolectricTestRunner.class)
#Config(constants = BuildConfig.class, sdk = 21, application = FakeCorePdfApplication.class)
public class PagerFragmentTest {
PagerFragment pagerFragment;
#Before
public void setup() {
pagerFragment = new PagerFragment();
CorePdfComponent component = ((CorePdfApplication)RuntimeEnvironment.application).getComponent();
doAnswer( new Answer() {
Object answer(InvocationOnMock invocation) {
fragment. pdfFactory = mock(PdfFactory.class);
return null;
}
}).when(component).inject(pageFragment);
startVisibleFragment(pagerFragment);
}
#Test
public void exists() throws Exception {
assertNotNull(pagerFragment);
}
}
You may try:
androidTestApt "com.google.dagger:dagger-compiler:<version>"
I was having similar problem, it worked for me.
when I try to replace dependencies using daggerMock, I get the following error:
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.test.espresso.intent.Intents.internalRelease()' on a null object reference
at android.support.test.espresso.intent.Intents.release(Intents.java:140)
at android.support.test.espresso.intent.rule.IntentsTestRule.afterActivityFinished(IntentsTestRule.java:68)
at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:260)
This is my Espresso Intents rule:
#Rule public IntentsTestRule<MyActivity> activityRule =
new IntentsTestRule<MyActivity>(MyActivity.class, true, false);
And this is my test
#Test
public void shouldDisplayToolbarOnStart() {
startActivity();
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
}
private MyActivity startActivity() {
return activityRule.launchActivity(null);
}
My Espresso custom rule:
public class EspressoDaggerMockRule extends DaggerMockRule<RootComponent> {
public EspressoDaggerMockRule() {
super(RootComponent.class, new MainModule(getApp()));
set(new DaggerMockRule.ComponentSetter<RootComponent>() {
#Override public void setComponent(RootComponent component) {
getApp().setComponent(component);
}
});
}
private static App getApp() {
return (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
}
}
Temporary workaround
Found a way to stop crashing:
#Rule public IntentsTestRule<MyActivity> activityRule =
new IntentsTestRule<MyActivity>(MyActivity.class, true, false);
replaced by
#Rule public ActivityTestRule<MyActivity> activityRule =
new ActivityTestRule<MyActivity>(MyActivity.class, true, false);
I want to use RoboGuice in a standard Android JUnit instrumentation test case and override one piece of my app's actual wiring with a mock for testing. I can't find anything online that explains how to do this as all of my search results go to Robolectric with RoboGuoice. I am not using Robolectric nor can I use it in my app for various reasons. Has anyone wired an app with RoboGuice and injected mocks for standard Android Intrumentation test cases?
I'm using the Roboguice 3 and I solved this problem with the following setup and teardown methods within the standard ActivityInstrumentationTestCase2.
Obviously you would need to replace new TestModule() in the snippet below with your own test module class.
#Override
protected void setUp() throws Exception {
super.setUp();
Application app = (Application)getInstrumentation().getTargetContext()
.getApplicationContext();
RoboGuice.getOrCreateBaseApplicationInjector(app, RoboGuice.DEFAULT_STAGE,
Modules.override(RoboGuice.newDefaultRoboModule(app))
.with(new TestModule()));
getActivity();
}
#Override
protected void tearDown() throws Exception {
RoboGuice.Util.reset();
super.tearDown();
}
I've managed to get it work in a simple usage way, you just bind dependencies inside rule using builder and may forget about them later, it will do everything by itself. You may think it's over engineered, but it's realy good for reusing if tyou have a many test classes with robo guice dependencies inside.
Usage in test classes looks like:
#Rule
public InjectWithMocksRule injectWithMocksRule = new InjectWithMocksRule(
this,
() -> new InjectRule
.BindingBuilder()
.add(MyClass.class, mockedClassImpl)
.add(SomeInterface.class, mockedInterfaceImpl));
I wrote helper class TestBindingModule:
public class TestBindingModule extends AbstractModule {
private HashMap<Class<?>, Object> bindings = new HashMap<Class<?>, Object>();
#Override
#SuppressWarnings("unchecked")
protected void configure() {
Set<Entry<Class<?>, Object>> entries = bindings.entrySet();
for (Entry<Class<?>, Object> entry : entries) {
bind((Class<Object>) entry.getKey()).toInstance(entry.getValue());
}
}
public void addBinding(Class<?> type, Object object) {
bindings.put(type, object);
}
public void addBindings(HashMap<Class<?>, Object> bindings) {
this.bindings.putAll(bindings);
}
public static void setUp(Object testObject, TestBindingModule module) {
Module roboGuiceModule = RoboGuice.newDefaultRoboModule(RuntimeEnvironment.application);
Module testModule = Modules.override(roboGuiceModule).with(module);
RoboGuice.getOrCreateBaseApplicationInjector(RuntimeEnvironment.application, RoboGuice.DEFAULT_STAGE, testModule);
RoboInjector injector = RoboGuice.getInjector(RuntimeEnvironment.application);
injector.injectMembers(testObject);
}
public static void tearDown() {
Application app = RuntimeEnvironment.application;
DefaultRoboModule defaultModule = RoboGuice.newDefaultRoboModule(app);
RoboGuice.getOrCreateBaseApplicationInjector(app, RoboGuice.DEFAULT_STAGE, defaultModule);
}
}
Than I use custom Rule to make it work easy:
public class InjectRule implements TestRule {
public interface BindingBuilderFactory {
BindingBuilder create();
}
public static class BindingBuilder {
private HashMap<Class<?>, Object> bindings = new HashMap<>();
public BindingBuilder add(Class<?> dependencyClass, Object implementation) {
bindings.put(dependencyClass, implementation);
return this;
}
HashMap<Class<?>, Object> buildBindings() {
return this.bindings;
}
}
private Object target;
private BindingBuilderFactory bindingBuilderFactory;
public InjectRule(Object target, BindingBuilderFactory bindingBuilderFactory) {
this.target = target;
this.bindingBuilderFactory = bindingBuilderFactory;
}
private void overrideTestInjections(Object target) {
TestBindingModule module = new TestBindingModule();
module.addBindings(this.bindingBuilderFactory.create().buildBindings());
TestBindingModule.setUp(target, module);
}
#Override
public Statement apply(Statement base, Description description) {
return new StatementDecorator(base);
}
private class StatementDecorator extends Statement {
private Statement baseStatement;
StatementDecorator(Statement b) {
baseStatement = b;
}
#Override
public void evaluate() throws Throwable {
before();
try {
baseStatement.evaluate();
} catch (Error e) {
throw e;
} finally {
after();
}
}
void after() {
TestBindingModule.tearDown();
}
void before() {
overrideTestInjections(target);
}
}
}
Also you may want to init mocks with #Mock annotation inside of your test classes, so you need another custom rule:
public class MockitoInitializerRule implements TestRule {
private Object target;
public MockitoInitializerRule(Object target) {
this.target = target;
}
#Override
public Statement apply(Statement base, Description description) {
return new MockitoInitializationStatement(base, target);
}
private class MockitoInitializationStatement extends Statement {
private final Statement base;
private Object test;
MockitoInitializationStatement(Statement base, Object test) {
this.base = base;
this.test = test;
}
#Override
public void evaluate() throws Throwable {
MockitoAnnotations.initMocks(test);
base.evaluate();
}
}
}
And, finaly, you want to combine them to mock mocks first and then set them as dependencies:
public class InjectWithMocksRule implements TestRule {
private final RuleChain delegate;
public InjectWithMocksRule(Object target, InjectRule.BindingBuilderFactory bindingBuilderFactory) {
delegate = RuleChain
.outerRule(new MockitoInitializerRule(target))
.around(new InjectRule(target, bindingBuilderFactory));
}
#Override
public Statement apply(Statement base, Description description) {
return delegate.apply(base, description);
}
}
I am trying to use Dagger in an Android functional test which inherits ActivityInstrumentationTestCase2.
The setup code looks like this:
#Override
protected void setUp() {
// TODO Auto-generated method stub
try {
super.setUp();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ObjectGraph.create(new TestModule()).inject(this);
this.activity = super.getActivity();
}`
The OnCreate method, which is fired by calling super.getActivity(), does not use the classes provided by the TestModule. But if I run my activity manually (outside of the testing context) then all the appropriate classes are provided/injected by my non-test module.
I found a way to use Dagger with ActivityInstrumentationTestCase2 by lazily creating the Object Graph. What I do is wait to create the Object Graph until the very first time a class wants to be injected, so as long as you add your modules before calling getActivity() (which starts the activity lifecycle for the activity under test) and use overrides = true in your test modules, this will work. Here's the relevant classes and snippets:
GraphHolder, as the name implies, holds the ObjectGraph object for us. We will make all calls to this class rather than directly to ObjectGraph.
public class GraphHolder {
private static GraphHolder sInstance;
private Object[] mModules;
private ObjectGraph mGraph;
private GraphHolder() {
}
public static GraphHolder getInstance() {
if (sInstance == null) {
sInstance = new GraphHolder();
}
return sInstance;
}
public void inject(Object object) {
if (mGraph == null) {
create();
}
mGraph.inject(object);
}
public <T> T get(Class<T> type) {
if (mGraph == null) {
create();
}
return mGraph.get(type);
}
public void addModules(Object... modules) {
if (mGraph != null) {
mGraph.plus(modules);
} else {
if (mModules == null) {
mModules = modules;
} else {
mModules = concatenate(mModules, modules);
}
}
}
private void create() {
mGraph = ObjectGraph.create(mModules);
mModules = null;
}
private Object[] concatenate(Object[] a, Object[] b) {
int aLength = a.length;
int bLength = b.length;
Object[] c = new Object[aLength + bLength];
System.arraycopy(a, 0, c, 0, aLength);
System.arraycopy(b, 0, c, aLength, bLength);
return c;
}
}
We'll add our modules in the Application class:
public class MyApplication extends Application {
#Override
public void onCreate() {
super.onCreate();
GraphHolder.getInstance().addModules(getModules());
}
Object[] getModules() {
return new Object[]{
// your modules here
};
}
}
Inside the classes we want to inject, we'll simply call GraphHolder.getInstance().inject(this) rather than ObjectGraph.inject(this)
In our test modules, we'll provide the objects we want to override for testing and add overrides = true to the #Module annotation. This tells the object graph to prefer this module's providers over others if there's a conflict.
Then, in our tests:
#Inject Foo mFoo;
#Override
public void setUp() {
super.setUp();
GraphHolder.getInstance().addModules(new TestFooModule());
GraphHolder.getInstance().inject(this); // This is when the object graph will be created
}
ObjectGraph.create(new TestModule()).inject(this);
This code is trying to inject dependencies created by TestModule into your TestCase instead of the tested Activity. What you'd have to do here is
ObjectGraph.create(new TestModule()).inject(this.activity);