Android Testing

Android testing: Mockito and Robolectric (Part 2)

Alex Zhukovich 12 min read
Android testing: Mockito and Robolectric (Part 2)
Table of Contents

This is a second article from series of articles about testing Android project. First article about "Unit testing" you can read here. Full source code you can find on Github. The project on GitHub contains different branches(basic version of app, unit tests for basic version of app, basic version + payment activity, mockito and robolectric tests

Challenge of unit testing

A Unit test should test a class in isolation. The achievement of this goal is completed because many Java classes depends on other classes. For solving this problem we use test doubles.

Classification of different objects

A dummy object is passed around but never used, that mean its methods are never used, object like this passed as a parameter.
A fake object has working implementation, but usually simplified.
A stub class is an partial implementation for a class or an interface with the purpose using it for testing.
A mock object is a dummy implementation for a class or an interface which defined for the methods calls.

Mock object generation

You can create a mock object manually or use a mock frameworks to simulate this class. As example, a mock object is a data provider. An application use real database in production, but for testing, the better way is to use a mock object which simulates the database and ensures that the test conditions are always the same.

Mocking frameworks

A mock frameworks allow to create mock object as simple as possible. Popular frameworks are EasyMock, jMock and Mockito.

Short introduction to Mockito framework

Mockito is a popular mock framework which can be used with JUnit. Mockito allows you to create and configure mock objects.

Adding mockito as dependency to the project

If you use gradle as your build system, add the following dependency to your build.gradle file.

dependencies {
   ...
   testCompile 'org.mockito:mockito-core:1.10.19'
}

If you use maven you can search for "org.mockito" and "mockito-core" via the Maven search website.

Using the Mockito API

Creating and configuring mock objects

Mockito allows use static mock() method for creating mock objects.

MyClass test = Mockito.mock(MyClass.class);</pre>

// or

@RunWith(MockitoJUnitRunner.class)
public class MockitoTest  {

     @Mock MyClass test;
     ...
}

The when(...).thenReturn(...) call allows to set up specific condition and return values for this term. You can also use methods like anyInt(), anyFloat(), anyList(), etc. to define an independency of input value and the method must return the certain value.

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

@Test
public void testGetMockedCoffeeId()  {
     //  create mock
    Coffee coffee = Mockito.mock(Coffee .class);
  
     // define return value for method getCoffeeId()
     when(test.getCoffeeId()).thenReturn(43);
  
     // use mock in test
     assertEquals(test.getCoffeeId(), 43);
}

// Demonstrates the return of multiple values
@Test
public void testMultipleValues() {
     List&lt;String&gt; list = new ArrayList&lt;&gt;();
     list.add("first");
     list.add("second");

     Iterator&lt;Integer&gt; iterator = Mockito.mock(Iterator.class);
     when(iterator.next()).thenReturn(0).thenReturn(1);

     assertEquals(list.get(0), list.get(iterator.next()));
     assertEquals(list.get(1), list.get(iterator.next()));
}


// this test demonstrates how to return values based on the input
@Test
public void testReturnValueDependentOnMethodParameter()  {
     final String MOCKITO = "mockito";
     final String ESPRESSO = "espresso";
     final int MOCKITO_INT = 1;
     final int ESPRESSO_INT = 2;

     Comparable comparable = mock(Comparable.class);
     when(comparable.compareTo(MOCKITO)).thenReturn(MOCKITO_INT);
     when(comparable.compareTo(ESPRESSO)).thenReturn(ESPRESSO_INT);

     assertEquals(MOCKITO_INT, comparable.compareTo(MOCKITO));
     assertEquals(ESPRESSO_INT, comparable.compareTo(ESPRESSO));
}

// this test demonstrates how to return values independent of the input value
@Test
public void testReturnValueInDependentOnMethodParameter()  {
     Comparable comparable = mock(Comparable.class);
     when(comparable.compareTo(anyInt())).thenReturn(-1);
     
     //assert
     assertEquals(-1 ,c.compareTo(9));
}

// return a value based on the type of the provide parameter
@Test
public void testReturnValueInDependentOnMethodParameter()  {
     Comparable c= mock(Comparable.class);
     when(c.compareTo(isA(String.class))).thenReturn(0);
     
     //assert
     Todo todo = new Todo(5);
     assertEquals(todo ,c.compareTo(new String(“test”)));
}

The doReturn(...).when(...).methodCall works similar but is useful for void methods.

@Test
public void spyLinkedListTest() {
     // Lets mock a LinkedList
     List list = new LinkedList();
     list.add("first");
     list.add("second");
     List spy = Mockito.spy(list);

     //You have to use doReturn() for stubbing
     doReturn("foo").when(spy).get(0);

     when(spy.get(0)).thenReturn("foo");

     assertEquals("foo", spy.get(0));
}

The doThrow variant can be used for methods which return void to through an exception.

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

@Test(expected=IOException.class)
public void IOExceptionTest() {
     // create an configure mock
     OutputStream mockStream = mock(OutputStream.class);
     doThrow(new IOException()).when(mockStream).close();
  
     // use mock
     OutputStreamWriter streamWriter= new OutputStreamWriter(mockStream);
     streamWriter.close();
}

Verify the calls on the mock objects

You can also use verify() method to ensure that the method was called.

@Test
public void verifyTest() {
     // create and configure mock
     Coffee coffee = mock(Coffee.class);
     when(coffee.getId()).thenReturn(43);

     // call method testing on the mock with parameter 12
     coffee.setId(12);
     coffee.getId();
     coffee.getId();

     // now check if method testing was called with the parameter 12
     verify(coffee).setId(Matchers.eq(12));

     // was the method called twice?
     verify(coffee, times(2)).getId();

     // other alternatives for verifiying the number of method calls for a method
     verify(coffee, never()).setDescription("never called");
     verify(coffee, atLeastOnce()).getId();
     verify(coffee, atLeast(2)).getId();
     verify(coffee, times(5)).setName("called five times");
     verify(coffee, atMost(2)).getId();
}

Limitations

  1. Final classes
  2. Anonymous classes
  3. Primitive types

I want to show you how we can test Android things.

@Test
public void shouldBeCreatedIntent() {
     Context context = Mockito.mock(Context.class);
     Intent intent = MainActivity.createQuery(context, "query", "value");
     assertNotNull(intent);
     Bundle extras = intent.getExtras();
     assertNotNull(extras);
     assertEquals("query", extras.getString("QUERY"));
     assertEquals("value", extras.getString("VALUE"));
}

Unfortunately if we run this test, we get next error log.

java.lang.RuntimeException: Method putExtra in android.content.Intent not mocked. See http://g.co/androidstudio/not-mocked for details.
 at android.content.Intent.putExtra(Intent.java)
 at PaymentActivity.createIntent(PaymentActivity.java:15)
 at MainActivityTest.shouldBeCreatedIntent(MainActivityTest.java:118)

Many basic methods from basic classes are not mocked. Fortunately you can use Robolectric.

Robolectric

Unit testing has been particularly difficult in Android, although with Robolectric it is much easier. This library also allows you to mock the Android SDK by removing the stubs that throw RuntimeExceptions.

Robolectric is a unit testing library that allows you to run your test in a Java Virtual Machine (JVM). This library also allow you to do Test Driven Development (TDD).

Robolectric replaced all Android classes by so-called shadow objects.

If a method is implemented by Robolectric, it forwards these method calls to the shadow object which act similar to the objects of the Android SDK. If a method is not implemented by the shadow object, it simply returns a default value, e.g., null or 0.

I want to show to you how to configure Robolectric with Android application. The easiest way is updating build.gradle file.

apply plugin: 'com.android.application'

android {
   compileSdkVersion 23
   buildToolsVersion "23.0.2"

   defaultConfig {
       minSdkVersion 14
       targetSdkVersion 23
       ...
       useLibrary 'org.apache.http.legacy'
       testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
       ...
   }
   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
       }
   }
}

dependencies {
   ...
   testCompile 'org.robolectric:robolectric:3.0'
   testCompile 'org.robolectric:shadows-support-v4:3.0'
}

If you want to use Shadow objects you must add this "org.robolectric:shadows-support-v4:3.0" dependency to your project and "useLibrary org.apache.http.legacy". Probably, you know that AndroidHttpClient was removed from the SDK in v23 of the build tools.

After it we can create first Android test with Robolectric. You need to create a new class for it in tests package name (android test package we use for instrumental test, test package we use for local JVM tests). Also need to setup Runner for our class and config.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {

     @Test
     public void shouldNotBeNull() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);
          assertNotNull(activity);
     }
}

We also must add few changes to the application. First of all, I want to add a payment activity for the application. I need to create a layout for it, an activity class and to add information about new activity to AndroidManifest.xml file.

The activity_payment layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout   
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical" android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:paddingLeft="@dimen/activity_horizontal_margin"
     android:paddingRight="@dimen/activity_horizontal_margin"
     android:paddingTop="@dimen/activity_vertical_margin"
     android:paddingBottom="@dimen/activity_vertical_margin">

     <TextView
           android:id="@+id/payment_data"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:textAppearance="?android:attr/textAppearanceLarge"
           android:text="@string/total_price" />
</LinearLayout>

The PaymentActivity class.

public class PaymentActivity extends AppCompatActivity {
     public final static String TOTAL_PRICE = "total_price";

     public static Intent createIntent(Context context, float totalPrice) {
          Intent intent = new Intent(context, PaymentActivity.class);
          intent.putExtra(TOTAL_PRICE, totalPrice);
          return intent;
     }

     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_payment);

          if (getIntent()!= null &amp;&amp; getIntent().getExtras() != null) {
               float totalPrice = getIntent().getExtras().getFloat(TOTAL_PRICE);
               TextView paymentData = (TextView) findViewById(R.id.payment_data);

               if (paymentData != null) {
                    paymentData.setText(getString(R.string.total_price, totalPrice));
               }
          }
     }
}

The last thing is updating AndroidManifest.xml file.

<application
     android:allowBackup="true"
     android:icon="@mipmap/ic_launcher"
     android:label="@string/app_name"
     android:supportsRtl="true"
     android:theme="@style/AppTheme">
   
     ...

     <activity android:name=".PaymentActivity"/>
</application>
Mockito and Robolectric test project

We must also update MainActivity and add a "Pay" button and the setOnClickListener to this button. For starting a new activity I use next line of code

startActivity(PaymentActivity.createIntent(getApplicationContext(), mOrder.getTotalPrice()));

In the first Robolectric test we checked if the MainActivity exists. First of all, we need to set up an activity. In this case, it’s MainActivity. Next step is comparing this object with a null.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
     @Test
     public void shouldNotBeNull() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);
          assertNotNull(activity);
     }
     ...
}

Next test case for verification of price labels. There are two labels for displaying a price in the application.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
     ...
     @Test
     public void shouldHavePriceLabels() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);

          TextView coffeePrice = (TextView) activity.findViewById(R.id.coffee_price);
          assertNotNull(coffeePrice);
          assertEquals(View.VISIBLE, coffeePrice.getVisibility());
          assertEquals(activity.getString(R.string.coffee_price, 5.0f), coffeePrice.getText());

          TextView totalPrice = (TextView) activity.findViewById(R.id.total_price);
          assertNotNull(totalPrice);
          assertEquals(View.VISIBLE, totalPrice.getVisibility());
          assertEquals(activity.getString(R.string.total_price, 0.0f), totalPrice.getText());
     }
     ...
}</pre>
<p>Next test case verifies a coffee cup picker (increment button + count label + decrement button).</p>
<pre class="EnlighterJSRAW" data-enlighter-language="java">@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
     ...
     @Test
     public void shouldHaveCoffeeCupPicker() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);

          Button incrementButton = (Button) activity.findViewById(R.id.coffee_increment);
          assertNotNull(incrementButton);
          assertEquals(View.VISIBLE, incrementButton.getVisibility());

          Button decrementButton = (Button) activity.findViewById(R.id.coffee_decrement);
          assertNotNull(decrementButton);
          assertEquals(View.VISIBLE, decrementButton.getVisibility());

          TextView coffeeCount = (TextView) activity.findViewById(R.id.coffee_count);
          assertNotNull(coffeeCount);
          assertEquals(View.VISIBLE, coffeeCount.getVisibility());
          assertEquals(activity.getString(R.string.default_coffee_count), coffeeCount.getText());
     }
     ...
}

Next test case verifies how to increment and decrement buttons work.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
   ...
     @Test
     public void shouldChangeCoffeeCupCountOnIncrementAndDecrementButtons() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);

          int count = 0;

          TextView coffeeCount = (TextView) activity.findViewById(R.id.coffee_count);
          assertNotNull(coffeeCount);
          assertEquals(View.VISIBLE, coffeeCount.getVisibility());
          assertEquals(String.valueOf(count), coffeeCount.getText());

          Button incrementButton = (Button) activity.findViewById(R.id.coffee_increment);
          assertNotNull(incrementButton);
          assertEquals(View.VISIBLE, incrementButton.getVisibility());

          Button decrementButton = (Button) activity.findViewById(R.id.coffee_decrement);
          assertNotNull(decrementButton);
          assertEquals(View.VISIBLE, decrementButton.getVisibility());

          incrementButton.performClick();
          assertEquals(String.valueOf(++count), coffeeCount.getText());

          decrementButton.performClick();
          assertEquals(String.valueOf(--count), coffeeCount.getText());

          //Should be previous value because count can be just positive
          decrementButton.performClick();
          assertEquals(String.valueOf(count), coffeeCount.getText());
     }
     ...
}

Next test case verifies if pay button exists.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
     ...
     @Test
     public void shouldHavePayButton() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);

          Button payButton = (Button) activity.findViewById(R.id.pay);
          assertNotNull(payButton);
          assertEquals(View.VISIBLE, payButton.getVisibility());
          assertEquals(activity.getString(R.string.pay), payButton.getText());
     }
     ...
}

Next test case verifies correctness of Intent for launching PaymentActivity.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
     ...   
     @Test
     public void shouldBeCreatedIntent() {
          final float TEST_PRICE = 12.0f;
          Context context = RuntimeEnvironment.application.getApplicationContext();

          Intent intent = PaymentActivity.createIntent(context, TEST_PRICE);
          assertNotNull(intent);
          Bundle bundle = intent.getExtras();
          assertEquals(TEST_PRICE, bundle.getFloat(PaymentActivity.TOTAL_PRICE), 0.00001f);
     }
     ...
}

The last test case for MainActivity starts PaymentActivity after click to "Pay" button.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
     ...
     @Test
     public void shouldStartActivityOnPayButton() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);
          Button payButton = (Button) activity.findViewById(R.id.pay);
          assertNotNull(payButton);
          payButton.performClick();

          ShadowActivity shadowActivity = shadowOf(activity);
          Intent startedIntent = shadowActivity.getNextStartedActivity();
          ShadowIntent shadowIntent = shadowOf(startedIntent);
          assertEquals(PaymentActivity.class.getName(),
          shadowIntent.getComponent().getClassName());
     }
}

You can find full source code of this class below.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {

     @Test
     public void shouldNotBeNull() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);
          assertNotNull(activity);
     }

     @Test
     public void shouldHavePriceLabels() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);

          TextView coffeePrice = (TextView) activity.findViewById(R.id.coffee_price);
          assertNotNull(coffeePrice);
          assertEquals(View.VISIBLE, coffeePrice.getVisibility());
          assertEquals(activity.getString(R.string.coffee_price, 5.0f), coffeePrice.getText());

          TextView totalPrice = (TextView) activity.findViewById(R.id.total_price);
          assertNotNull(totalPrice);
          assertEquals(View.VISIBLE, totalPrice.getVisibility());
          assertEquals(activity.getString(R.string.total_price, 0.0f), totalPrice.getText());
     }

     @Test
     public void shouldHaveCoffeeCupPicker() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);

          Button incrementButton = (Button) activity.findViewById(R.id.coffee_increment);
          assertNotNull(incrementButton);
          assertEquals(View.VISIBLE, incrementButton.getVisibility());

          Button decrementButton = (Button) activity.findViewById(R.id.coffee_decrement);
          assertNotNull(decrementButton);
          assertEquals(View.VISIBLE, decrementButton.getVisibility());

          TextView coffeeCount = (TextView) activity.findViewById(R.id.coffee_count);
          assertNotNull(coffeeCount);
          assertEquals(View.VISIBLE, coffeeCount.getVisibility());
          assertEquals(activity.getString(R.string.default_coffee_count), coffeeCount.getText());
     }

     @Test
     public void shouldChangeCoffeeCupCountOnIncrementAndDecrementButtons() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);

          int count = 0;

          TextView coffeeCount = (TextView) activity.findViewById(R.id.coffee_count);
          assertNotNull(coffeeCount);
          assertEquals(View.VISIBLE, coffeeCount.getVisibility());
          assertEquals(String.valueOf(count), coffeeCount.getText());

          Button incrementButton = (Button) activity.findViewById(R.id.coffee_increment);
          assertNotNull(incrementButton);
          assertEquals(View.VISIBLE, incrementButton.getVisibility());

          Button decrementButton = (Button) activity.findViewById(R.id.coffee_decrement);
          assertNotNull(decrementButton);
          assertEquals(View.VISIBLE, decrementButton.getVisibility());

          incrementButton.performClick();
          assertEquals(String.valueOf(++count), coffeeCount.getText());

          decrementButton.performClick();
          assertEquals(String.valueOf(--count), coffeeCount.getText());

          //Should be previous value because count can be just positive
          decrementButton.performClick();
          assertEquals(String.valueOf(count), coffeeCount.getText());
     }

     @Test
     public void shouldHavePayButton() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);

          Button payButton = (Button) activity.findViewById(R.id.pay);
          assertNotNull(payButton);
          assertEquals(View.VISIBLE, payButton.getVisibility());
          assertEquals(activity.getString(R.string.pay), payButton.getText());
     }

     @Test
     public void shouldBeCreatedIntent() {
          final float TEST_PRICE = 12.0f;

          Context context = RuntimeEnvironment.application.getApplicationContext();
          Intent intent = PaymentActivity.createIntent(context, TEST_PRICE);
          assertNotNull(intent);
          Bundle bundle = intent.getExtras();
          assertEquals(TEST_PRICE, bundle.getFloat(PaymentActivity.TOTAL_PRICE), 0.00001f);
     }

     @Test
     public void shouldStartActivityOnPayButton() {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);
          Button payButton = (Button) activity.findViewById(R.id.pay);
          assertNotNull(payButton);
          payButton.performClick();

          ShadowActivity shadowActivity = shadowOf(activity);
          Intent startedIntent = shadowActivity.getNextStartedActivity();
          ShadowIntent shadowIntent = shadowOf(startedIntent);
          assertEquals(PaymentActivity.class.getName(),
               shadowIntent.getComponent().getClassName());
     }
}

Unfortunately, this source code is not really good. I will show how to improve this code. As you can see from previous examples, almost all of the methods have creation and initialization code of activity. Many other tests create and initialize views, like TextView and Buttons, all these parts of code we can move to method with @Before annotation because method with @Before annotation starts before each test. Next code snippet contain all changes in the MainActivityTest class.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {

     private MainActivity mActivity;
     private TextView mCoffeePrice;
     private TextView mTotalPrice;
     private Button mIncrementButton;
     private Button mDecrementButton;
     private TextView mCoffeeCount;
     private Button mPayButton;
     private Context mContext;

     @Before
     public void setUp() {
          mContext = RuntimeEnvironment.application.getApplicationContext();
          mActivity = Robolectric.setupActivity(MainActivity.class);

          mCoffeePrice = (TextView) mActivity.findViewById(R.id.coffee_price);
          mTotalPrice = (TextView) mActivity.findViewById(R.id.total_price);
          mIncrementButton = (Button) mActivity.findViewById(R.id.coffee_increment);
          mDecrementButton = (Button) mActivity.findViewById(R.id.coffee_decrement);
          mCoffeeCount = (TextView) mActivity.findViewById(R.id.coffee_count);
          mPayButton = (Button) mActivity.findViewById(R.id.pay);
     }

     @Test
     public void shouldNotBeNull() {
          assertNotNull(mActivity);
     }

     @Test
     public void shouldHavePriceLabels() {
          assertNotNull(mCoffeePrice);
          assertEquals(View.VISIBLE, mCoffeePrice.getVisibility());
          assertEquals(mActivity.getString(R.string.coffee_price, 5.0f), mCoffeePrice.getText());

          assertNotNull(mTotalPrice);
          assertEquals(View.VISIBLE, mTotalPrice.getVisibility());
          assertEquals(mActivity.getString(R.string.total_price, 0.0f), mTotalPrice.getText());
     }

     @Test
     public void shouldHaveCoffeeCupPicker() {
          assertNotNull(mIncrementButton);
          assertEquals(View.VISIBLE, mIncrementButton.getVisibility());

          assertNotNull(mDecrementButton);
          assertEquals(View.VISIBLE, mDecrementButton.getVisibility());

          assertNotNull(mCoffeeCount);
          assertEquals(View.VISIBLE, mCoffeeCount.getVisibility());
          assertEquals(mActivity.getString(R.string.default_coffee_count), mCoffeeCount.getText());
     }

     @Test
     public void shouldChangeCoffeeCupCountOnIncrementAndDecrementButtons() {
          int count = 0;

          assertNotNull(mCoffeeCount);
          assertEquals(View.VISIBLE, mCoffeeCount.getVisibility());
          assertEquals(String.valueOf(count), mCoffeeCount.getText());

          assertNotNull(mIncrementButton);
          assertEquals(View.VISIBLE, mIncrementButton.getVisibility());

          assertNotNull(mDecrementButton);
          assertEquals(View.VISIBLE, mDecrementButton.getVisibility());

          mIncrementButton.performClick();
          assertEquals(String.valueOf(++count), mCoffeeCount.getText());

          mDecrementButton.performClick();
          assertEquals(String.valueOf(--count), mCoffeeCount.getText());

          //Should be previous value because count can be just positive
          mDecrementButton.performClick();
          assertEquals(String.valueOf(count), mCoffeeCount.getText());
     }

     @Test
     public void shouldHavePayButton() {
          assertNotNull(mPayButton);
          assertEquals(View.VISIBLE, mPayButton.getVisibility());
          assertEquals(mActivity.getString(R.string.pay), mPayButton.getText());
     }

     @Test
     public void shouldBeCreatedIntent() {
          final float TEST_PRICE = 12.0f;

          Intent intent = PaymentActivity.createIntent(mContext, TEST_PRICE);
          assertNotNull(intent);
          Bundle bundle = intent.getExtras();
          assertEquals(TEST_PRICE, bundle.getFloat(PaymentActivity.TOTAL_PRICE), 0.00001f);
     }

     @Test
     public void shouldStartActivityOnPayButton() {
          assertNotNull(mPayButton);
          mPayButton.performClick();

          ShadowActivity shadowActivity = shadowOf(mActivity);
          Intent startedIntent = shadowActivity.getNextStartedActivity();
          ShadowIntent shadowIntent = shadowOf(startedIntent);
          assertEquals(PaymentActivity.class.getName(),
               shadowIntent.getComponent().getClassName());
     }
}

Before writing next tests you must know that we can get created activity in some ways. First of all, you can create an activity with setupActivity(Class<T> activityClass) method, but it not a the best way if you want to pass additional parameter to the Activity via Intent. Another way is using buildActivity(Class<T> activityClass) method with next sequence of methods: withIntent(SOME_INTENT), create(), start(), resume, visible(), and get(); Of course, you can add a change or add some methods to the sequence. You must also know that setupActivity method can do next sequence of actions: create(), start(), postCreate(null), resume(), visible(), and get().

MainActivity mActivity = Robolectric.setupActivity(MainActivity.class);
MainActivity mActivity = Robolectric.buildActivity(MainActivity.class)
    .withIntent(MainActivity.createIntent(RuntimeEnvironment.application, TOTAL_PRICE))
    .create()
    .start()
    .resume()
    .visible()
    .get();

We can start to write tests for the PaymentActivity.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class PaymentActivityTest {
     private static final float TOTAL_PRICE = 15.0f;

     private PaymentActivity mActivity;
     private TextView mTotalPrice;

     @Before
     public void setUp() {
          mActivity = Robolectric.buildActivity(PaymentActivity.class)
              .withIntent(PaymentActivity.createIntent(RuntimeEnvironment.application, TOTAL_PRICE))
              .create()
              .start()
              .resume()
              .visible()
              .get();

          mTotalPrice = (TextView) mActivity.findViewById(R.id.payment_data);
     }

     @Test
     public void shouldNotBeNull() {
          assertNotNull(mActivity);
     }

     @Test
     public void shouldHaveTotalPrice() {
          assertNotNull(mTotalPrice);
          assertEquals(View.VISIBLE, mTotalPrice.getVisibility());
          assertEquals(mActivity.getString(R.string.total_price, TOTAL_PRICE), mTotalPrice.getText());
     }
}

We finished with all tests for this application. Right now, we can run all tests. As you can see all tests works correctly.

Source code separates to different branches on GitHub

Resources:

Thank you for your time.


Mobile development with Alex

A blog about Android development & testing, Best Practices, Tips and Tricks

Share
More from Mobile development with Alex

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Mobile development with Alex.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.