Today we’re going to get into testing Android apps with Robolectric. This one is quite tricky, so let’s get to it

Testing View of MVP

If you followed my previous posts, we learned how to unit test some of our code with Mockito, in particular, Presenter part. Because by itself Presenter is quite modular and easy to instantiate with mocks

Now View holds all the references to layout views, RecyclerView adapters, Presenter etc. You could extract it just like we did with Presenter. So it would look something like that

public class ChatView implementes ChatContract.View { private EditText messageInput; private ImageView sendButton; // adapters and more other stuff public ChatView(EditText messageInput, ImageView sendButton, ...) { this.messageInput = messageInput; this.sendButton = sendButton; } // getters, setters, override methods } 1 2 3 4 5 6 7 8 9 10 11 12 public class ChatView implementes ChatContract . View { private EditText messageInput ; private ImageView sendButton ; // adapters and more other stuff public ChatView ( EditText messageInput , ImageView sendButton , . . . ) { this . messageInput = messageInput ; this . sendButton = sendButton ; } // getters, setters, override methods }

And then you instantiate it in activity/tests. This might seem like complete overhead. But since by using MVP we already design our app in a completely different way from simple app architecture, it’s just an architecture solution. And now we can unit test our View part with Mockito just like we did with Presenter, right?

Nope, even though I mocked my RecyclerView.Adapter implementation class, I still got a NPE on calling #notifyIitemInserted (parent’s method). Obviously, there’re some bugs with Mockito when extending Android RecyclerView.Adapter

Testing notifying RecyclerView.Adapter is one of the tests for my View, so having a test suite without it would be incomplete. That’s why I recommend testing View with Robolectric

What’s Robolectric

They advertise it as a unit testing framework. It runs locally on your computer and has access to Android framework’s classes. I think it’s just as integrated testing as it is unit one. Because with Robolectric you can instantiate your activity and find views by ids, set their properties, check listeners and more

Which is totally fine, it doesn’t really matter in which category lies those tests, with Robolectric you can test your app completely basically. They run much slower than regular local tests. Initial spin up takes me about 20 – 30 secs, tests themselves take around 200 ms though. So I wouldn’t say that it’s in the TDD kind of area, where you can easily run tests during each method implementations. Still faster than using Espresso which runs on Android device

Code

Let’s get started. First I want to test correct implementations of my ChatContract.View. It handles updating layout with its public methods. And delegates work to Presenter on user events

First, add Robolectric dependency

testCompile "org.robolectric:robolectric:3.6.1" 1 testCompile "org.robolectric:robolectric:3.6.1"

Since ChatContrat.View is implemented by activity – we can just use this method

ChatActivity activity = Robolectric.setupActivity(ChatActivity.class); 1 ChatActivity activity = Robolectric . setupActivity ( ChatActivity . class ) ;

It runs all the initial activity lifecycle methods (onCreate, onStart, onResume…). So the layout is inflated and you can just find views by ids in the test itself. Which is great, this means we don’t need getters for them. We still need setters for activity fields like adapters, array lists, because we’re going to mock them.

Here’s my test suite

@RunWith(RobolectricTestRunner.class) public class ChatActivityTest { public static final String MESSAGE = "Normal message"; private ChatActivity activity; private EditText messageInput; private ImageView sendButton; @Mock private MessagesAdapter messagesAdapter; @Mock private ChatPresenter presenter; @Before public void setUp() { MockitoAnnotations.initMocks(this); activity = Robolectric.setupActivity(ChatActivity.class); messageInput = activity.findViewById(R.id.message_input); sendButton = activity.findViewById(R.id.send_button); activity.setAdapter(messagesAdapter); activity.setPresenter(presenter); when(listOfMessages.size()).thenReturn(0); } @Test public void clearMessageInput_WasEmpty_ClearedMessageInput() { activity.clearMessageInput(); assertThat(messageInput.getText().toString(), is("")); } @Test public void clearMessageInput_WastntEmpty_ClearedMessageInput() { messageInput.setText("Some text"); activity.clearMessageInput(); assertThat(messageInput.getText().toString(), is("")); } @Test public void enableSendButton_WasDisabled_ButtonEnabled() { presetSendButtonAsDisabled(); activity.enableSendButton(); assertThatSendButtonIsEnabled(); } @Test public void enableSendButton_WasEnabled_ButtonEnabled() { activity.enableSendButton(); assertThatSendButtonIsEnabled(); } @Test public void disableSendButton_WasEnabled_ButtonDisabled() { activity.disableSendButton(); assertThatSendButtonIsDisabled(); } @Test public void disableSendButton_WasDisabled_ButtonDisabled() { presetSendButtonAsDisabled(); activity.disableSendButton(); assertThatSendButtonIsDisabled(); } @Test public void notifyItemAdded_CalledAdapterMethod() { activity.notifyItemAdded(4); verify(messagesAdapter).notifyItemInserted(4); } private void presetSendButtonAsDisabled() { sendButton.setEnabled(false); sendButton.setAlpha(0.5f); } private void assertThatSendButtonIsEnabled() { assertThat(sendButton.getAlpha(), is(1.0f)); assertThat(sendButton.isEnabled(), is(true)); } private void assertThatSendButtonIsDisabled() { assertThat(sendButton.getAlpha(), is(0.5f)); assertThat(sendButton.isEnabled(), is(false)); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 @RunWith ( RobolectricTestRunner . class ) public class ChatActivityTest { public static final String MESSAGE = "Normal message" ; private ChatActivity activity ; private EditText messageInput ; private ImageView sendButton ; @Mock private MessagesAdapter messagesAdapter ; @Mock private ChatPresenter presenter ; @Before public void setUp ( ) { MockitoAnnotations . initMocks ( this ) ; activity = Robolectric . setupActivity ( ChatActivity . class ) ; messageInput = activity . findViewById ( R . id . message_input ) ; sendButton = activity . findViewById ( R . id . send_button ) ; activity . setAdapter ( messagesAdapter ) ; activity . setPresenter ( presenter ) ; when ( listOfMessages . size ( ) ) . thenReturn ( 0 ) ; } @Test public void clearMessageInput_WasEmpty_ClearedMessageInput ( ) { activity . clearMessageInput ( ) ; assertThat ( messageInput . getText ( ) . toString ( ) , is ( "" ) ) ; } @Test public void clearMessageInput_WastntEmpty_ClearedMessageInput ( ) { messageInput . setText ( "Some text" ) ; activity . clearMessageInput ( ) ; assertThat ( messageInput . getText ( ) . toString ( ) , is ( "" ) ) ; } @Test public void enableSendButton_WasDisabled_ButtonEnabled ( ) { presetSendButtonAsDisabled ( ) ; activity . enableSendButton ( ) ; assertThatSendButtonIsEnabled ( ) ; } @Test public void enableSendButton_WasEnabled_ButtonEnabled ( ) { activity . enableSendButton ( ) ; assertThatSendButtonIsEnabled ( ) ; } @Test public void disableSendButton_WasEnabled_ButtonDisabled ( ) { activity . disableSendButton ( ) ; assertThatSendButtonIsDisabled ( ) ; } @Test public void disableSendButton_WasDisabled_ButtonDisabled ( ) { presetSendButtonAsDisabled ( ) ; activity . disableSendButton ( ) ; assertThatSendButtonIsDisabled ( ) ; } @Test public void notifyItemAdded_CalledAdapterMethod ( ) { activity . notifyItemAdded ( 4 ) ; verify ( messagesAdapter ) . notifyItemInserted ( 4 ) ; } private void presetSendButtonAsDisabled ( ) { sendButton . setEnabled ( false ) ; sendButton . setAlpha ( 0.5f ) ; } private void assertThatSendButtonIsEnabled ( ) { assertThat ( sendButton . getAlpha ( ) , is ( 1.0f ) ) ; assertThat ( sendButton . isEnabled ( ) , is ( true ) ) ; } private void assertThatSendButtonIsDisabled ( ) { assertThat ( sendButton . getAlpha ( ) , is ( 0.5f ) ) ; assertThat ( sendButton . isEnabled ( ) , is ( false ) ) ; } }

As you see, I set view properties just in the test itself and then verify correct implementation of my methods by retrieving view properties and checking them. Rather than what we did with Presenter by using Mockito.verify (check that mock’s object method was called)

And views are returned to default state on each test because @Before method is called before each test, meaning layout is being inflated each time. So no need to reset anything

Here’s the View implementations

public class ChatActivity extends AppCompatActivity implements ChatContract.View { private ChatContract.Presenter presenter; private MessagesAdapter adapter; private List<String> listOfMessages; private EditText messageInput; private ImageView sendButton; private RecyclerView recyclerView; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); messageInput = findViewById(R.id.message_input); sendButton = findViewById(R.id.send_button); recyclerView = findViewById(R.id.list); ... } @Override public void notifyItemAdded(int position) { adapter.notifyItemInserted(position); } @Override public void clearMessageInput() { messageInput.setText(""); } @Override public void enableSendButton() { if (!sendButton.isEnabled()) { sendButton.setEnabled(true); sendButton.setAlpha(1.0f); } } @Override public void disableSendButton() { if (sendButton.isEnabled()) { sendButton.setEnabled(false); sendButton.setAlpha(0.5f); } } public void setPresenter(ChatContract.Presenter presenter) { this.presenter = presenter; } public void setAdapter(MessagesAdapter adapter) { this.adapter = adapter; } public void setListOfMessages(List<String> listOfMessages) { this.listOfMessages = listOfMessages; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class ChatActivity extends AppCompatActivity implements ChatContract . View { private ChatContract . Presenter presenter ; private MessagesAdapter adapter ; private List <String> listOfMessages ; private EditText messageInput ; private ImageView sendButton ; private RecyclerView recyclerView ; protected void onCreate ( Bundle savedInstanceState ) { super . onCreate ( savedInstanceState ) ; setContentView ( R . layout . activity_main ) ; messageInput = findViewById ( R . id . message_input ) ; sendButton = findViewById ( R . id . send_button ) ; recyclerView = findViewById ( R . id . list ) ; . . . } @Override public void notifyItemAdded ( int position ) { adapter . notifyItemInserted ( position ) ; } @Override public void clearMessageInput ( ) { messageInput . setText ( "" ) ; } @Override public void enableSendButton ( ) { if ( ! sendButton . isEnabled ( ) ) { sendButton . setEnabled ( true ) ; sendButton . setAlpha ( 1.0f ) ; } } @Override public void disableSendButton ( ) { if ( sendButton . isEnabled ( ) ) { sendButton . setEnabled ( false ) ; sendButton . setAlpha ( 0.5f ) ; } } public void setPresenter ( ChatContract . Presenter presenter ) { this . presenter = presenter ; } public void setAdapter ( MessagesAdapter adapter ) { this . adapter = adapter ; } public void setListOfMessages ( List <String> listOfMessages ) { this . listOfMessages = listOfMessages ; } }

Again, this is more of an integrated testing, but it’s way easier to do it this way than mocking all the activity views and adding setters for them. I still figuring out which way of testing I will prefer

Testing Listeners

And this part I separated because here we’ll test if View called correct methods of Presenter. It happens in view listers and with Robolectric you can easily test that

@Test public void typeSomeText_PassedToPresenter() { messageInput.setText(MESSAGE); verify(presenter).messageInputTextChanged(MESSAGE); } @Test public void pressedSendButton_EmptyInput_PassedEmptyString() { sendButton.performClick(); verify(presenter).sendMessage(""); } @Test public void pressedSendButton_NormalStringInput_PassedCorrectString(){ messageInput.setText(MESSAGE); sendButton.performClick(); verify(presenter).sendMessage(MESSAGE); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void typeSomeText_PassedToPresenter ( ) { messageInput . setText ( MESSAGE ) ; verify ( presenter ) . messageInputTextChanged ( MESSAGE ) ; } @Test public void pressedSendButton_EmptyInput_PassedEmptyString ( ) { sendButton . performClick ( ) ; verify ( presenter ) . sendMessage ( "" ) ; } @Test public void pressedSendButton_NormalStringInput_PassedCorrectString ( ) { messageInput . setText ( MESSAGE ) ; sendButton . performClick ( ) ; verify ( presenter ) . sendMessage ( MESSAGE ) ; }

And here’s the implementation

public class ChatActivity extends AppCompatActivity implements ChatContract.View { private ChatContract.Presenter presenter; private MessagesAdapter adapter; private List<String> listOfMessages; private EditText messageInput; private ImageView sendButton; private RecyclerView recyclerView; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); messageInput = findViewById(R.id.message_input); sendButton = findViewById(R.id.send_button); messageInput.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { presenter.messageInputTextChanged(s.toString()); } @Override public void afterTextChanged(Editable s) { } }); sendButton.setOnClickListener(v -> presenter.sendMessage(messageInput.getText().toString())); } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class ChatActivity extends AppCompatActivity implements ChatContract . View { private ChatContract . Presenter presenter ; private MessagesAdapter adapter ; private List <String> listOfMessages ; private EditText messageInput ; private ImageView sendButton ; private RecyclerView recyclerView ; protected void onCreate ( Bundle savedInstanceState ) { super . onCreate ( savedInstanceState ) ; setContentView ( R . layout . activity_main ) ; messageInput = findViewById ( R . id . message_input ) ; sendButton = findViewById ( R . id . send_button ) ; messageInput . addTextChangedListener ( new TextWatcher ( ) { @Override public void beforeTextChanged ( CharSequence s , int start , int count , int after ) { } @Override public void onTextChanged ( CharSequence s , int start , int before , int count ) { presenter . messageInputTextChanged ( s . toString ( ) ) ; } @Override public void afterTextChanged ( Editable s ) { } } ) ; sendButton . setOnClickListener ( v -> presenter . sendMessage ( messageInput . getText ( ) . toString ( ) ) ) ; } . . . }

Testing listeners might seem unnecessary because it’s just delegation, but it was pretty easy, why not include them as well

Conclusion

Alright, I hope now you understand where Robolectric stands in the testing. It’s a unique tool and feels like instrumented UI tests running locally. You can get the source code here