Closed nuxzero closed 6 years ago
@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<MainActivity> injectedFactory = new SingleActivityFactory<MainActivity>(MainActivity.class) {
@Override
protected MainActivity create(Intent intent) {
MainActivity activity = new MainActivity();
activity.viewModelFactory = testApp.daggerTestAppComponent.vmFactory();
return activity;
}
};
@Rule
public ActivityTestRule<MainActivity> 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
public class TestApp {
public TestAppComponent daggerTestAppComponent;
@Override
public void onCreate() {
super.onCreate();
daggerTestAppComponent = DaggerTestAppComponent.builder().application(this).build();
}
}
In your test classes:
testApp = ((TestApp) InstrumentationRegistry.getTargetContext().getApplicationContext());
//you can get the vmFactory instance with:
testApp.daggerTestAppComponent.vmFactory();
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
}
}
class TestApp : App()
{
lateinit var daggerTestAppComponent: TestAppComponent
override fun onCreate() {
super.onCreate()
daggerTestAppComponent = DaggerTestAppComponent.builder()
.application(this)
.baseUrl(BuildConfig.BASE_URL)
.build()
}
}
@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
}
@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
}
@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?
@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.
I'm try to setup UI testing similar GithubBrowserSample and look like sample project have only mock
ViewModel
forFragment
but forActivity
it doesn't.So, on my code
activityRule.activity.viewModelFactory = createViewModelFor(viewModel)
it doesn't set beforeonCreate()
in Activity, meant it doesn't use mock ViewModel.Can someone help me about this issue please?