Android testing with Espresso, Cucumber and Dagger

Android testing with Espresso, Cucumber and Dagger

software_development

Espresso is an Android module that allows to perform in a test environment UI actions like touch and swipe and check the UI state. Cucumber is a framework that allows users to do BDD tests on Android using the Gherkin language. Dagger is a dependency injection framework made by Google for Android. Using Espresso with Cucumber you can test your app’s feature step by step using the actual UI. Plus, thanks to Dagger, you can mock your services and make your tests independent and reliable.

Dependency injection with Dagger

Add these lines to the dependencies of your project:


dependencies {
[...]
    compile 'com.google.dagger:dagger:2.2'
    apt 'com.google.dagger:dagger-compiler:2.2'


[...]
    androidTestApt 'com.google.dagger:dagger-compiler:2.2'
}

Note: the apt and androidTestApt must be placed respectively after the compile and androidTestCompile steps

Now let’s implemented Dagger. First of all, create your services:


package com.my.app.service;
public class FooService {
	public FooService() {}
}

After that, you have to create a special class called Module that will inject them in your application:


package com.my.app.module;
 
import com.my.app.service.FooService; 
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
 
@Module
public class ExampleModule {
    
    @Provides 
    @Singleton
    FooService provideFooService() {
        return new FooService();
    }
}

The method with @Provides will be called every time that you want to inject a service into a class. To request an injection you have to add the annotation @Inject. Example:


public class ExampleActivity {
    @Inject
    FooService fooService;
}

If you run the code you will notice that the service is not injected yet. We have to create an interface called Component that will connects your @Provides with your @Inject.


@Singleton
@Component(modules = {ExampleModule.class})
public interface ExampleComponent {
    void inject(ExampleActivity activity);
}

A class that implements this interface will be generated at compile time (thanks to the apt steps that we added in the gradle file). To retrieve the component object we can add this to the Application class:


public class ExampleApplication extends Application {
    private ExampleComponent component;

    @Override
    public void onCreate() {
        super.onCreate();
        component = createComponent();
    }

    protected ApplicationComponent createComponent() {
        return DaggerExampleComponent.builder()
                .androidModule(new ExampleModule(this))
                .build();
    }

    public ExampleComponent component() {
        return component;
    }
}

Note: The real component will always be named Dagger<componen-interface-name>

 

Now in the activity onCreate method we inject the services:


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ((ExampleApplication) getApplication()).component().inject(this);
}

BDD with Espresso and Cucumber

To install Espresso and Cucumber add these dependencies:


dependencies {
[...]
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2') {
        exclude module: 'support-annotations'
        exclude module: 'appcompat-v7'
        exclude module: 'design'
    }
    androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2.2') {
        exclude module: 'support-annotations'
        exclude module: 'support-v4'
        exclude module: 'recyclerview-v7'
        exclude module: 'design'
    }
    androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.2'
    androidTestCompile 'info.cukes:cucumber-android:1.2.0@jar'
    androidTestCompile 'info.cukes:cucumber-picocontainer:1.2.0'
}

In the folder src/androidTest/assets/features we add our tests. Here’s an example:


Feature: User can open the app

	Scenario: User views the welcome message
		When I visit the main activity
		Then I should see "Hello world!"

If we try to run these tests we will see an error. That’s because we have to create a special test runner. Add this class to src/androidTest/java/my.package.test/runner


package my.package.test.runner;

import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import android.support.test.runner.MonitoringInstrumentation;

import my.package.test.InstrumentedTestExampleApplication;

import cucumber.api.android.CucumberInstrumentationCore;

import cucumber.api.CucumberOptions;

@CucumberOptions(
        features = "features",
        glue = {"my.package.test.steps"},
        format = {"pretty",
                "html:/data/data/my.package/cucumber-reports/cucumber-html-report",
                "json:/data/data/my.package/cucumber-reports/cucumber.json",
                "junit:/data/data/my.package/cucumber-reports/cucumber.xml"
        }
)
public class CucumberTestRunner extends MonitoringInstrumentation {

    private final CucumberInstrumentationCore mInstrumentationCore = new CucumberInstrumentationCore(this);

    @Override
    public void onCreate(Bundle arguments) {
        super.onCreate(arguments);

        mInstrumentationCore.create(arguments);
        start();
    }

    @Override
    public void onStart() {
        super.onStart();

        waitForIdleSync();
        mInstrumentationCore.start();
    }

    @Override
    public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        return super.newApplication(cl, InstrumentedTestExampleApplication.class.getName(), context);
    }
}

We must also be sure that this runner will be used so we add this to our gradle.build


defaultConfig {
[...]
            testInstrumentationRunner 'my.package.test.runner.CucumberTestRunner'
}

As you can see we also created a test Application (InstrumentedTestExampleApplication) that extends the main one. Time to implement the steps. We create a MainActivitySteps class on src/androidTest/java/my.package.test.steps


import android.test.InstrumentationTestCase;

public class MainActivitySteps extends InstrumentationTestCase {
    private MainActivity activity;

    @After
    public void tearDown() throws Exception {
        ActivityFinisher.finishOpenActivities();
        if (activity != null){
            activity.finish();
        }
        super.tearDown();
    }

    @When("^I visit the main activity$")
    public void iVisitTheMainActivity() throws Throwable {
        this.activity = launchActivity("my.package", MainActivity.class, null);
    }
}

In the step method we simply launch the activity and in the tearDown method we make sure that after the tests are finished the activity is also finished. There is another one step left to implement. It’s a step that can be used on any kind of activity so we add a generic CommonSteps class


import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

public class CommonSteps {
    @Then("^I should see \"([^\"]*)\"$")
    public void iShouldSee(String text) throws Throwable {
        onView(withText(text)).check(matches(isDisplayed()));
    }
}

That’s where the Espresso magic happens. Here we check that in the current activity exists a view that has the text we passed to the steps and that is displayed. Now rerun the tests and see the results!

Dependency injection inside the Cucumber tests

Let’s say that we want to use in the MainActivity our FooService and make it return a mocked value. Using Dagger we can create a Component and a Module inside the test application. Let’s briefly see all the classes.


src/androidTest/java/my.package.test.module/TestExampleModule

package my.package.test.module;

@Module
public class TestExampleModule{

    @Provides
    @Singleton
    CompanyService provideFooService() {
        return new FooServiceStub();
    }
}

src/androidTest/java/my.package.test.component/TestAExamplepplicationComponent

package my.package.test.component;

@Singleton
@Component(modules = {TestAndroidModule.class})
public interface TestAExamplepplicationComponent extends ApplicationComponent {
    void inject(CommonSteps steps);
}

src/androidTest/java/my.package.test/InstrumentedTestExampleApplication

package my.package.test;

public class InstrumentedTestExampleApplication extends ExampleApplication {
    @Override
    protected TestAExamplepplicationComponent createComponent() {
        return DaggerTestAExamplepplicationComponent.builder()
                .testAndroidModule(new TestExampleModule())
                .build();
    }
}

In the test android module we have replaced the FooService with a stub that holds our test logic. We can now inject the the stubbed service to our steps classes.


public class CommonSteps {
    @Inject
    FooService fooService;

    @Before
    public void beforeScenario() {
        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
        ExampleApplication app = (ExampleApplication) instrumentation.getTargetContext().getApplicationContext();
        InstrumentedTestExampleApplication component = (InstrumentedTestExampleApplication) app.component();
        component.inject(this);
    }
}

Now we can finally create steps where we inject our mocked values


@Given("^The service returns \"([^\"]*)\"$")
public void theServiceReturns(String text) throws Throwable {
    fooService.setReturnValue(text);
}

Conclusions

As you can see, using Dagger, Espresso and Cucumber gives us the power to fully test our application logic and program with Behaviour Driven Development on Android. Also, if we want to test the internal logic of our services, we can still create old-fashioned unit tests.

 

Related News