Architecture-components-samples: Mock ViewModel in Activity for UI Testing with Espresso

Created on 27 Dec 2017  路  14Comments  路  Source: android/architecture-components-samples

I'm try to setup UI testing similar GithubBrowserSample and look like sample project have only mock ViewModel for Fragment but for Activity it doesn't.

So, on my code activityRule.activity.viewModelFactory = createViewModelFor(viewModel) it doesn't set before onCreate() in Activity, meant it doesn't use mock ViewModel.

```kotlin @RunWith(AndroidJUnit4::class)
class MainActivityTest {

val viewModel = mock(MainViewModel::class.java)

@Rule
@JvmField
val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java, true, true)

private val liveData = MutableLiveData<Resource<Object>>()

@Before
open fun setUp() {
    activityRule.activity.viewModelFactory = createViewModelFor(viewModel)
    `when`(viewModel.liveData).thenReturn(liveData)
    viewModel.liveData?.observeForever(mock(Observer::class.java) as Observer<Resource<Object>>)
    liveData.postValue(Resource.success(Object()))
}

fun <T : ViewModel> createViewModelFor(model: T): ViewModelProvider.Factory =
    object : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            if (modelClass.isAssignableFrom(model.javaClass)) {
                return model as T
            }
            throw IllegalArgumentException("unexpected model class " + modelClass)
        }
    }

}
```
Can someone help me about this issue please?

Most helpful comment

Use the following class to create the activity instance:
https://developer.android.com/reference/android/support/test/runner/intercepting/SingleActivityFactory.html
Then you can create different factories for mocked/injected activities.
Example with dagger2 injection:

```
private SingleActivityFactory injectedFactory = new SingleActivityFactory(MainActivity.class) {
@Override
protected MainActivity create(Intent intent) {
MainActivity activity = new MainActivity();
activity.viewModelFactory = testApp.daggerTestAppComponent.vmFactory();
return activity;
}
};

@Rule
public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(injectedFactory, false, false);

All 14 comments

@nuxzero did you find a solution for this?

@brillmt Not yet

Use the following class to create the activity instance:
https://developer.android.com/reference/android/support/test/runner/intercepting/SingleActivityFactory.html
Then you can create different factories for mocked/injected activities.
Example with dagger2 injection:

```
private SingleActivityFactory injectedFactory = new SingleActivityFactory(MainActivity.class) {
@Override
protected MainActivity create(Intent intent) {
MainActivity activity = new MainActivity();
activity.viewModelFactory = testApp.daggerTestAppComponent.vmFactory();
return activity;
}
};

@Rule
public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(injectedFactory, false, false);

@kekefigure can you explain testApp.daggerTestAppComponent.vmFactory() detail ?

Of course. Let's say you have a AppComponent class in your application module, then you define a TestAppComponent(which extends AppComponent) for your instrumented tests.

TestAppComponent.java

@Singleton
@Component(modules = {
...
})
public interface TestAppComponent extends AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(Application application);

        TestAppComponent build();
    }

    void inject(TestApp myApp);

   //you can acces this instance from the created TestAppComponent 
    ViewModelProvider.Factory vmFactory();
}

 ```
**TestApp.java**
```java
public class TestApp {
public TestAppComponent daggerTestAppComponent;
    @Override
    public void onCreate() {
        super.onCreate();
        daggerTestAppComponent = DaggerTestAppComponent.builder().application(this).build();
    }
}
 ```
**In your test classes:**
```java
 testApp = ((TestApp) InstrumentationRegistry.getTargetContext().getApplicationContext());
//you can get the vmFactory instance with:
testApp.daggerTestAppComponent.vmFactory();

Note: This solution can cause errors in dagger2 dependency graph, so its just a workaround.

I can't see what I'm doing wrong, does this look ok? Would I be correct in saying that this test factory overrides the normal injected one? The activity vm factory and the test one have different hash codes, I was thinking that means they are still different objects when they shouldn't be.

Build.gradle has test runner info:
testInstrumentationRunner "core.sdk.util.MyTestRunner"

kaptAndroidTest "com.google.dagger:dagger-android-processor:${versions.dagger}"
androidTestImplementation "com.android.support.test:runner:${versions.runner}"

class MyTestRunner : AndroidJUnitRunner() {
    override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
        return super.newApplication(cl, TestApp::class.java.name, context)
    }
}
@OpenForTesting
class App : Application(), HasActivityInjector {
    @Inject
    lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>

    override fun onCreate() {
        super.onCreate()

        DaggerAppComponent.builder()
                .application(this)
                .baseUrl(BuildConfig.BASE_URL)
                .build()
                .inject(this)
    }

    override fun activityInjector(): DispatchingAndroidInjector<Activity> {
        return activityDispatchingAndroidInjector
    }
}



md5-ed14cdc7255b98eb6f923b817449a452



class TestApp : App()
{
    lateinit var daggerTestAppComponent: TestAppComponent

    override fun onCreate() {
        super.onCreate()
        daggerTestAppComponent = DaggerTestAppComponent.builder()
                .application(this)
                .baseUrl(BuildConfig.BASE_URL)
                .build()
    }
}



md5-ed14cdc7255b98eb6f923b817449a452



@Singleton
@Component(modules = [AndroidInjectionModule::class, AppModule::class, NetworkModule::class, ActivityBuilder::class])
interface AppComponent {

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: Application): Builder
        @BindsInstance
        fun baseUrl(@Named("baseUrl") baseUrl: String): Builder
        fun build(): AppComponent
    }

    fun inject(app: App)
    fun Repository(): Repository
}



md5-ed14cdc7255b98eb6f923b817449a452



@Singleton
@Component(modules = [AndroidInjectionModule::class, AppModule::class, NetworkModule::class, ActivityBuilder::class])
interface TestAppComponent {

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: Application): Builder
        @BindsInstance
        fun baseUrl(@Named("baseUrl") baseUrl: String): Builder
        fun build(): TestAppComponent
    }

    fun inject(app: TestApp)
    fun vmFactory(): ViewModelProvider.Factory
}



md5-ed14cdc7255b98eb6f923b817449a452



@RunWith(AndroidJUnit4::class)
class LoginActivityTest {

    private val email = "*********@gmail.com"
    private val password = "*****"

    private val injectedFactory = object : SingleActivityFactory<LoginActivity>(LoginActivity::class.java) {
        override fun create(intent: Intent): LoginActivity {
            val activity = LoginActivity()
            val testApp = InstrumentationRegistry.getTargetContext().applicationContext as TestApp
            activity.viewModelFactory = testApp.daggerTestAppComponent.vmFactory()
            return activity
        }
    }

    @Suppress("MemberVisibilityCanBePrivate")
    @get:Rule
    val activityRule = ActivityTestRule(injectedFactory,false, false)

    private lateinit var viewModel:LoginViewModel
    private val user = MutableLiveData<Resource<User>>()

    @Before
    @Throws(Throwable::class)
    fun init() {
        viewModel = Mockito.mock(LoginViewModel::class.java)
        `when`(viewModel.user).thenReturn(user)
        doNothing().`when`(viewModel).setLogin(anyString(), anyString())

        val intent = Intent(InstrumentationRegistry.getTargetContext(), LoginActivity::class.java)
        activityRule.launchActivity(intent)
        EspressoTestUtil.disableProgressBarAnimations(activityRule)
    }

    @Test
    fun loading(){
        //When: we are in a loading state
        user.postValue(Resource.loading(null))
        //Then: our progress bar is showing, with login text
        onView(withId(R.id.progress_bar)).check(matches(isDisplayed()))
    }
}

If you use the same vm factory as in the normal app module, then the normal factory gets injected every time by the AppInjector(which then overrides the mock instance).

If you want to use the normal vm factory and mocks just for specific test cases, then you need to register an ActivityLifecycleCallbacks in the TestApp class.

TestApp.class

public class TestApp {
  public ViewModelProvider.Factory viewModelFactory;
    TestApp.this.registerActivityLifecycleCallbacks({
   ....
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

                if (activity instanceof LoginActivity) {
                    LoginActivity loginActivity = (LoginActivity) activity;

                    if (viewModelFactory != null) {
                        loginActivity.viewModelFactory = viewModelFactory;
                    }
                }
            }
   })

}

In tests where mock instance needed:

@Before
public void init() {
    //reset before every test, so it doesn't override the injected one
    testApp.viewModelFactory = null;
    when(mockedLoginViewModelInstance).thenReturn(...);
}

//test where you need a mocked instance
@Test
public void someTest(){
    testAppInstance.viewModelFactory = ViewModelUtil.createFor(mockedLoginViewModelInstance);
}

So basically every test uses the same vm factory as in your normal app, but you can specify a mock instance where it is needed.

Hope it helps!

@kekefigure where is under package the TestAppComponent and TestApp class ? or androidTest ?

Both class are belong to androidTest. TestAppComponent extends the AppComponent and TestApp extends the custom Application class.

@kekefigure i haven't understand, can you open source the sample in the case ?

@chasel can you create a sample project which is based on your project structure?

screen shot 2018-06-04 at 13 51 08

screen shot 2018-06-04 at 13 52 55
@kekefigure ViewModelFactory can't inject in the RegChannelNewActivityTest class.

Just wanted to mention I did manage to do this thanks to @kekefigure 's good answer. It still had problems and I have a theory a good reason for Google now advocating a single Activity approach to development is partially due to the weird edge cases and non trivial boiler plate to test VMs in Activities when Dagger is involved.

The answer required mocking the ViewModel, App, App Injector and VM Factory and overriding onActivityCreated in the App Injector, and while the tests were fine, it broke the production code at which point I stopped fighting it and converted everything to a bloomin' Fragment so I don't have to worry about this any more 馃槃

@danielwilson1702 Can you please post your final working code? I'm currently trying to mock an activity's viewmodel and haven't been able to achieve it.

Was this page helpful?
0 / 5 - 0 ratings