Reaching out for help since I have been beating my head against a wall on an issue I have never seen before.
I have a standard fragment activity, using espresso for testing. I have a static manager in an associated module library linked to by the app.
The Issue:
When the test runs, it makes a static call into my ServiceManager. When the code is executed in onCreateView, in the stack trace it is calling a different method in the manager, not the one it should. Weirder yet, it is passing arguments that make no sense when tracing. The code is as follows (items simplified to illustrate what is going on).
ServiceManager.java - comes from a project library
public class ServiceManager {
private static final ServiceManager INSTANCE = new ServiceManager();
private UserManager userManager;
public static initForTest(Context context) {
// some storage initialization here
INSTANCE.userManager = new UserManagerImpl();
}
public static ServiceManager getInstance() {
return INSTANCE;
}
public static UserManager getUserManager() {
return INSTANCE.userManager;
}
}
UserManager.java - comes from a project library
public interface UserManager {
void registerListener(UserListener listener);
void refreshDataFromSource(String id);
}
UserManagerImpl.java - comes from a project library
public class UserManagerImpl implements UserManager {
public void registerListener(UserListener listener) {
// code really does not matter here, but I have a listener manager
ManagerListeners.register(UserListener.class, this);
}
public void refreshDataFromSource(String id) {
// kicks off an async task of mine
Log.i(TAG, "initiating refresh for %s", id);
UserRefreshTask task = new UserRefreshTask(
new TaskCallbackWithReturn<Consumer>() {
@Override
public void done(Consumer consumer) {
refreshAllListeners(true);
}
@Override
public void fail() {
refreshAllListeners(false);
}
}, id);
task.execute();
}
}
NewUserStartFragment.java - in the app project
public class UserProfileStartFragment extends Fragment implements UserListener {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Get the view from fragment_newuserstart.xml
View view = inflater.inflate(R.layout.fragment_newuserstart, container, false);
// register listeners
ServiceManager.getInstance().getUserManager().registerListener(this);
// rest does not matter since we don't get to it.
return view;
}
}
The backing (dead simple) activity NewUserActivity.java - from the app project
public class NewUserActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_newuser);
if (savedInstanceState == null) {
FragmentUtils.replace(this, R.id.fragment_userscreen, new UserProfileStartFragment());
}
Log.i(TAG, "Activity created");
}
}
And a simple test NewUserStartFragmentTest.java - tests in the app project
@RunWith(AndroidJUnit4.class)
public class UserProfileStartFragmentTest {
// on activity rule, activity initialization delayed on false
@Rule
public ActivityTestRule<NewUserActivity> mActivityRule =
new ActivityTestRule<>(NewUserActivity.class, true, false);
private NewUserActivity userActivity;
@Before
public void setUp() throws Exception {
Instrumentation instrumentation
= InstrumentationRegistry.getInstrumentation();
Context context = instrumentation.getTargetContext();
// service manager at this point will be initialized
ServiceManager.initForTest(context);
}
@After
public void tearDown() throws Exception {
// ensure activity is dead
ActivityUtils.finish(userActivity);
ServiceManager.reset();
}
@Test
public void testInitialScreen() throws Exception {
userActivity = mActivityRule.launchActivity(new Intent());
onView(withId(R.id.image_profile_photo)).check(matches(notNullValue()));
onView(withId(R.id.button_email)).check(matches(notNullValue()));
onView(withId(R.id.button_existing)).check(matches(notNullValue()));
onView(withId(R.id.button_cancel)).check(matches(notNullValue()));
}
}
And now the wierdness
When the test runs, it always reports the following.
java.lang.IllegalArgumentException: consumerId must not be null or empty
at com.google.common.base.Preconditions.checkArgument(Preconditions.java:122)
at com.arryved.android.applibrary.tasks.UserRefreshTask.<init>(UserRefreshTask.java:31)
**at com.arryved.android.applibrary.manager.usermanager.UserManagerImpl.refreshDataFromSource(UserManagerImpl.java:300)
at com.arryved.emptor.fragments.UserProfileStartFragment.onCreateView(UserProfileStartFragment.java:41)**
at android.support.v4.app.Fragment.performCreateView(Fragment.java:1965)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1078)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1259)
at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:738)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1624)
at android.support.v4.app.FragmentController.execPendingActions(FragmentController.java:330)
at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:547)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1220)
at android.support.test.runner.MonitoringInstrumentation.callActivityOnStart(MonitoringInstrumentation.java:546)
at android.app.Activity.performStart(Activity.java:5953)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2261)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2360)
at android.app.ActivityThread.access$800(ActivityThread.java:144)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
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:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
UM, IT SHOULD NOT BE EXECUTING refreshDataFromSource. IT SHOULD BE EXECUTING registerListener.
So, I then decided to add a stop point and debug. what is stranger, it is executing the code, but not passing a string, rather passing the fragment as the argument. Java variable typing alone should cause this to fail! This tells me it has to be a byte code mix up. Is it dex? Is it something else? I have never seen anything like this before.
I have tried on a MacOs android studio and a Linux android studio. I have done build cleans both from the command line and from studio. I have run against several different emulators and an actual Nexus 5 device. Does not matter, same results which tells me it is confused after build and not because of code.
Oh, and another point of reference. Just running the app the fragment works as expected. This seems to be occurring when I just run the test. (But I worry there is something else sinister going on in dex so I don't trust the app unless I have this figured out in tests).
Here is my build.gradle for the app.
apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
applicationId "com.arryved.emptor"
minSdkVersion 19
targetSdkVersion 23
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// Enabling multidex support.
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/maven/com.google.guava/guava/pom.properties'
exclude 'META-INF/maven/com.google.guava/guava/pom.xml'
}
dexOptions {
javaMaxHeapSize "4g"
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.1.0'
compile 'com.android.support:recyclerview-v7:23.1.0'
androidTestCompile 'com.android.support.test:runner:0.4'
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.1') {
exclude group: 'com.google.guava', module: 'guava'
}
// add this for intent mocking support
androidTestCompile('com.android.support.test.espresso:espresso-intents:2.2') {
exclude group: 'com.google.guava', module: 'guava'
}
// add this for webview testing support
androidTestCompile('com.android.support.test.espresso:espresso-web:2.2.1') {
exclude group: 'com.google.guava', module: 'guava'
}
compile project(':AppLibrary')
}
And for my AppLibrary build.gradle
apply plugin: 'com.android.library'
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
}
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
minSdkVersion 19
targetSdkVersion 23
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// Enabling multidex support.
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
exclude 'META-INF/LICENSE.txt'
}
dexOptions {
javaMaxHeapSize "4g"
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:multidex:1.0.0'
compile("com.google.android.gms:play-services:8.3.0") {
exclude group: 'com.android.support', module: 'support-v4'
}
compile 'com.android.support:appcompat-v7:23.1.0'
compile 'com.android.support:recyclerview-v7:23.1.0'
androidTestCompile 'com.android.support.test:runner:0.4'
androidTestCompile 'com.android.support.test:rules:0.4'
}
--------------UPDATE---------------
Just to see what happens, I updated the service manager to deal in the hard implementation (UserManagerImpl) not the interface (UserManager). Tests pass when using the implementation not the interface reference. So, at this point the best I can ascertain is that during the linking cycle, android or dex is getting confused about the interface. And when I change it back, the test is back to failing as shown above (thus not a random build issue either).