Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to inject Mocked Presenter of Activity in Instrumentation testing with Espresso

I have been trying this for a week. And I have crawled every article available but their implementations or examples fall short or stop at the steps of Espresso Tests.

My Android Application follows MVP architecture (And is in Java)

Scenario: [Giving just one example] I have a HomeActivity which gets a HomePresenter using Dagger2. (Provides method in the HomeModule exposed through a void inject(HomeActivity activity) in the HomeComponent.

In my espressoTest for HomeActivity I would like to inject a mockpresent. I Have not exposed this dependencies inside an AppModule through an AppComponent. which most examples on the net do (So they just create a new testApplication and then do the needfull)

I do not want to use the productFlavours way of injecting or providing mockclasses as it doesnt give me control over the Mockito.when methods.

So basically. I would like to inject a mockpresenter wherein i can do whatever Mockito.when()s on it for the sake of my unit tests in espresso.

My Code is below.

HomeComponent

@HomeScope
@Component(modules = HomeModule.class,dependencies = AppComponent.class)
public interface HomeComponent {
    void inject(HomeActivity activity);
}

HomeModule

@Module
public class HomeModule {

    private final IHomeContract.View view;

    public HomeModule(IHomeContract.View view) {
        this.view = view;
    }

    @Provides
    @HomeScope
    public IHomeContract.Presenter presenter(FlowsRepository flowsRepository, UserRepository userRepository, LoanRepository loanRepository) {
        return new HomePresenter(view, flowsRepository, userRepository, loanRepository);
    }

}

AppComponent

@Component(modules = {AppModule.class,RepositoryModule.class})
@AppScope
public interface AppComponent {
    void inject(App app);

    FlowsRepository flowRepository();
    LoanRepository loanRepository();
    UserRepository userRepository();
}

AppModule

@Module
public class AppModule {
    private Context appContext;

    public AppModule(@NonNull Context context) {
        this.appContext = context;
    }

    @Provides
    @AppScope
    public Context context() {
        return appContext;
    }
}

App

component = DaggerAppComponent.builder()
                .appModule(new AppModule(this))
                .build();
        component.inject(this);

HomeActivity

HomeComponent component = DaggerHomeComponent.builder()
                .appComponent(((App) getApplication()).getComponent())
                .homeModule(new HomeModule(this))
                .build();

Once again. In my tests (espresso) i would like to inject a mockedHomePresenter the set by Mockito. So I can just unit test my views.

like image 382
yUdoDis Avatar asked Jun 29 '18 22:06

yUdoDis


1 Answers

The key point in resolving the problem is to have such a Dagger Module that provides a mock Presenter in HomeActivity's instrumented test instead of the "real" one.

For this the following 2 extra actions need to be done (you might also want to see an example).

  1. Delegate instantiation of HomeActivity's Component to some abstraction.
  2. Substitute the implementation of the abstraction in instrumented tests to provide mocks.

I'll use Kotlin in the example below.

Define the delegate interface:

interface HomeComponentBuilder {
    fun build(view: IHomeContract.View): HomeComponent
}

Move the HomeComponent initialisation from HomeActivity to the delegate implementation:

class HomeComponentBuilderImpl constructor(private val app: App) : HomeComponentBuilder {

override fun build(view: IHomeContract.View): HomeComponent =
    DaggerHomeComponent.builder()
        .homeModule(HomeModule(view))
        .build()
}

Make the delegate be in application "scope" so that you could interchange its implementation for instrumented tests:

interface App {
    val homeComponentBuilder: HomeComponentBuilder
    ...
}

App implementation should now contain

class AppImpl : Application(), App {
    override val homeComponentBuilder: HomeComponentBuilder by lazy {
        HomeComponentBuilderImpl(this@AppImpl)
    }
    ...
}

Component initialisation in HomeActivity looks as follows:

(application as App)
        .homeComponentBuilder
        .build(this)
        .inject(this)

For instrumented testing create TestHomeComponent that extends HomeComponent:

@HomeScope
@Component(modules = [TestHomeModule::class])
interface TestHomeComponent : HomeComponent

where TestHomeModule provides a mock Presenter

@Module
class TestHomeModule {

    @Provides
    fun providePresenter(): IHomeContract.Presenter = mock()
}

What's left to do is to make a test delegate implementation

class TestHomeComponentBuilderImpl : HomeComponentBuilder {
    override fun build(view: IHomeContract.View): HomeComponent =
        DaggerTestHomeComponent.builder()
             .testTestHomeModule(TestHomeModule())
             .build()
}

and initialise it in TestAppImpl

class TestAppImpl : Application(), App {
    override val homeComponentBuilder: HomeComponentBuilder by lazy {
        TestHomeComponentBuilderImpl()
    }
    ...
}

The rest is standard. Create a custom AndroidJUnitRunner that uses TestAppImpl:

class TestAppRunner : AndroidJUnitRunner() {
    override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application = Instrumentation.newApplication(TestAppImpl::class.java, context)
}

and add it to app module build.gradle

defaultConfig {
    testInstrumentationRunner "your.package.TestAppRunner"
    ...
}

Usage example:

@RunWith(AndroidJUnit4::class)
class HomeActivityTest {
    private lateinit var mockPresenter: IHomeContract.Presenter

    @get:Rule
    val activityRule = ActivityTestRule(HomeActivity::class.java)

    @Before
    fun setUp() {
        mockPresenter = activityRule.activity.presenter
    }

    @Test
    fun activity_onCreate_presenter_should_onViewCreated() {
        verify(mockPresenter).someMethod()
    }
}
like image 92
Onik Avatar answered Sep 29 '22 04:09

Onik