📜 ⬆️ ⬇️

Design patterns for Android development. Part 3 - User Interface, Testing, AndroidMock

In the last article I explained what MVP is and how to organize an application development process using MVP. Now I will show you how I developed my T-Alarm.

At first I made a presentation and presenter, as described in the last article.

View


Naturally, my view is a successor to the class of Activity, more precisely RoboActivity, what I am talking about in brief. Below is a very typical source code for the alarm settings editing window:

public class AlarmEdit extends RoboActivity implements IAlarmEdit { @InjectView(R.id.ae_et_AlarmName) EditText etName; @Inject AlarmEditPresenter presenter; @InjectView(R.id.ae_btn_Save) Button btnSave; @InjectView(R.id.ae_btn_Cancel) Button btnCancel; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.alarm_edit); presenter.onCreate(this); btnSave.setOnClickListener(this); btnCancel.setOnClickListener(this); } @Override public void onClick(View btn) { AlarmEdit.ClickSourceItem clickItem = AlarmEdit.ClickSourceItem.CANCEL; switch (btn.getId()) { case R.id.ae_btn_Save: clickItem = AlarmEdit.ClickSourceItem.SAVE; break; case R.id.ae_btn_Cancel: clickItem = AlarmEdit.ClickSourceItem.CANCEL; break; } presenter.onClick(clickItem); } @Override public String getAlarmName() { return etName.getText().toString(); } @Override public void setAlarmName(String alarmName) { etName.setText(alarmName); } @Override public Bundle getViewBundle() { return getIntent().getExtras(); } } 

So let's get down to this example.
Let's start with what RoboActivity is and why it is needed here. RoboActivity is included with RoboGuice. RoboGuice is a Dependency Injection Framework for Android.
More details on RoboGuice are available on the project website:
code.google.com/p/roboguice/wiki/SimpleExample?tm=6
')
In this class, I use RoboGuice to avoid the oddities in the onCreate method, like:
 etName = (EditText) findViewById(R.id.ae_et_AlarmName); 

RoboGuice does it all for me when creating activity by processing lines with Inject tags ..., for example:
 @InjectView(R.id.ae_et_AlarmName) EditText etName; 

I give the name of the resources as follows:
<abbr. activity name> _ <abbr. name of control type> _ <control name>
So it is easier to target among a large heap of resource identifiers, which of the entire application are dumped into one namespace "R.id".

Also I create Presenter here using RoboGuice through the following ad:
 @Inject AlarmEditPresenter presenter; 

In fact, RoboGuice has a few more uses that I will show next.

Thus, at the time of the onCreate () method execution, all the fields in my class are already initialized and I do not need to bother with their initialization separately, my code looks nicer.

In the onCreate () method, I only register the current class as OnClickListener and transfer to the presenter a link to this presentation class, so that the presenter will further control the presentation properties. Due to this, Inversion of Control is obtained when the business logic in the presenter controls the presentation.
I do this to test business logic, which is concentrated in a separate class presenter, which is weakly dependent on other parts of the application. Then I will show how when testing I tear out the presenter from the program's environment, connect various stubs and imitations instead of other components and perform tests over the presenter.

The onClick () method shows that all commands that the presentation receives from the user are transmitted to the presenter. Here I’ll only convert the identifiers of the buttons to commands, so that when debugging I’ll see the names of the commands, not the numbers corresponding to the resources in the R.id class, and it’s not worth adding any presentations to the presenter.

Here the presentation communicates directly with the presenter, without using an interface. I did so because I do not need to replace the presenter with imitation. I do not test performance in unit tests.

The setAlarmName () and getAlarmName () methods are used by the presenter to display and get the name of the alarm in the editing window.

The getViewBundle () method is needed to get the ID of the edited alarm clock, which I pass through Intent. The alarm clock itself comes from the model that the repository loads, while all the alarm clocks are loaded into the model at once, because there are not many of them. The model is loaded in the first activity of the program, there is a list of alarm clocks and I already need to have a model with all the alarms.
Through the use of Roboguice, I have a singleton model. That is, in the whole application I have one copy of the model and I do not need to transfer it from activity to activity. The presenter of each activity gets a model from Roboguice:
 @Inject IAlarmListModel alarmListModel; 

Since Singleton is not implemented through a static class, but through RoboGuice, I can replace the model in the tests with the simulation I need.

The code presented here and in the interface is the entire additional code that I had to write using MVP. The rest of the code that is in presenter should be written anyway, because it contains the necessary business logic.
In other words, the MVP fee is a few mapping lines, which is not much compared to what we get when using MVP.

Intefrace IAlarmEdit


The interface is extremely simple and simply contains methods for the presenter to communicate with the presentation, as well as to replace the presentation with imitation in tests.
 public interface IAlarmEdit { public enum ClickSourceItem { SAVE, CANCEL } public abstract String getAlarmName(); public abstract void setAlarmName(String alarmName); public abstract Bundle getViewBundle() } 


How Presenter Works


Starting the implementation of the user interface, I wanted to make it as fast as possible, because no one likes the brake programs. At first, I planned to concentrate as much logic as possible in the service so as not to load the user interface. But then I discovered that the service is running in the same thread as the user interface. And for using separate threads there is a great thing AsyncTask. It was through her that I did multithreading in the program, and both in the user interface and in the service.

Therefore, in the presenter there is an instance of the class IModelLoader, which asynchronously calls the repository and passes it a model for saving. In more detail about asynchronous reading / loading I will write in the following articles.
 public class AlarmEditPresenter implements IModelReciver { @Inject public IModelLoader modelSaver; private int id; private AlarmEdit view; public void onCreate(IAlarmEdit alarmEdit) { //   ,   //   this.view = alarmEdit; // id    Bundle bundle = view.getViewBundle(); id = bundle.getInt(MainScreenPresenter.BUNDLE_ARG_ALARM_ID); //   ,     // ,      //    . alarmListModel.takeAlarmForEdit(id); //     view.setAlarmName(alarmListModel.getEditingAlarm().getName()); //  ,   //     modelSaver.setReciver(this); } public void onClick(AlarmEdit.ClickSourceItem clickItem) { switch(clickItem) { case SAVE: // ,    alarmListModel.getEditingAlarm().setName(view.getAlarmName()); //     alarmListModel.updateAlarm(alarmListModel.getEditingAlarm()); //   . modelSaver.saveModel(); //   . //     . break; case CANCEL: //     . view.setResult(Activity.RESULT_CANCELED); view.finish(); break; } } @Override public void update(String event) { if(event.equals(IModelLoader.EVENT_ALARM_MODEL_SAVE)) { // ,   . // . view.setResult(Activity.RESULT_OK); view.finish(); } } } 

In fact, I still have a lot of code in this presenter, but there is nothing new in terms of MVP in it, so I didn’t give it.

I note only that in the presenter there is also such a member of the class:
 @Inject private IDateHolder dateHolder; 

I need IDateHolder to simulate the time and locale I need in tests, and in a real application, Roboguice substitutes a class that returns the system time and locale.
In particular, on the main window there is the current time - this is a string that displays the time in the current locale. I have tests in which I imitate the time and locale I need and check that the string has gone to the view in the correct locale. Thanks to these tests, I am sure that my program works correctly in different locales.

Presenter test


Due to the fact that in the presenter all the connections with the other components are made through interfaces, I can simulate these interfaces in tests. What am I going to do now.

In this test, I have a task: check AlarmEditPresenter. I need to check that he takes the ID from the form, takes the desired alarm clock from the model and correctly displays its name in the edit control.
 public class TestAlarmEditPresenter extends RoboUnitTestCase<TestApplication> { public class AlarmEditModule extends AbstractAndroidModule { //       presenter- //   RoboGuice    presenter  //          @Override protected void configure() { bind(AlarmEditPresenter.class); bind(IAlarmListModel.class) .toProvider(AlarmListModelProvider.class); } } private IAlarmListModel model; @Override protected void setUp() throws Exception { TestApplication.alarmTestModule = new AlarmEditModule(); super.setUp(); model = AndroidMock.createNiceMock(IAlarmListModel.class); AlarmListModelProvider.alarmListModel = model; } //    private void initializeTest() { AndroidMock.reset(model); } /** *          View * @throws Exception */ @MediumTest public void testAlarmEditOpen() throws Exception { initializeTest(); AlarmItem alarm = new AlarmItem(); alarm.setName("Test Name"); //,   2     model.takeAlarmForEdit(2); //  ,    presenter AndroidMock.expect(model.getEditingAlarm()).andStubReturn(alarm); AndroidMock.replay(model); //    Intent   //,     2 Bundle bundle = new Bundle(); bundle.putInt(MainScreenPresenter.BUNDLE_ARG_ALARM_ID, 2); AlarmEdit view = AndroidMock.createMock(AlarmEdit.class); AndroidMock.expect(view.getViewBundle()).andStubReturn(bundle); //      View view.setAlarmName("Test Name"); AndroidMock.replay(view); //  presenter AlarmEditPresenter presenter = getInjector().getInstance(AlarmEditPresenter.class); //  presenter.onCreate(view); //,      ,    AndroidMock.verify(view); //,        AndroidMock.verify(model); } } 

For the nested class AlarmEditModule and its configure () method, see the following articles for more details on using RoboGuice.

In the setUp () method, I create an imitation and put it in the provider. In general, why do I need a provider: with its help, I solve the following problem. When initializing fields marked with the Inject attribute, it is necessary to substitute the necessary simulations. RoboGuice, by the type of field, will find that it is necessary to use such a provider (see the configure () method), through which it will substitute the desired imitation.

In the initializeTest () method, which is performed before each test, I clear the state of all simulations after other tests to ensure the isolation of the tests.

In the test itself, I first prepare imitations:
I create an alarm clock and tell the model to imitate that if the presenter asks for an editable alarm clock, then return to him this prepared alarm clock:
 AndroidMock.expect(view.getViewBundle()).andStubReturn(bundle); 

I also indicate that I expect certain calls:
 //,   2     model.takeAlarmForEdit(2); ..... //      View view.setAlarmName("Test Name"); 

After that, an instance of the AlarmEditPresenter class is created via RoboGuice. You must create it through RoboGuice so that it fills all the fields marked with the Inject attribute with the desired values.
Then I call the method presenter.onCreate (view) in which the alarm clock is loaded and its name is transferred to the view.

And in the end I check that presenter made all the necessary calls:

 //,      ,    AndroidMock.verify(view); //,        AndroidMock.verify(model); 

To clarify the remaining details, I will tell about:

AndroidMock


This is a framework for creating imitations. You can read more about it here:
code.google.com/p/android-mock

Here I will explain what AndroidMock is and what features of its use I found in my project.

All Mock Framework in all languages ​​are designed for the following:

First, make a simulation of a given interface or class. Simulation is an instance of an interface or class and can be passed to all methods where an instance of the original interface or class is required. Immediately after creation, imitation has all fields, albeit with values ​​of 0 or null, depending on the type, and you can also call any method that is also 0 or null, depending on the type.

If it doesn’t matter what is called in imitation, or imitation is simply not used in the test, but the class under test is not created without imitation, then in that case an empty imitation is created in one line and passed to the class being created.

Secondly. For imitation, you can specify in one line that if such a method is called, then return such and such a value. In the example above, this is the line:
 AndroidMock.expect(view.getViewBundle()).andStubReturn(bundle); 

Typically, this imitation feature is used when the class implementing the interface is not yet ready, or when the method is ready, but in order for it to work properly, you need to prepare it for a long time. And in order not to bother with preparing the data for a method that, as a rule, is not even the subject of a test, it is easier to simply simulate the result returned by it.
By the way, if I had expected two calls, and they would have to return different results, I would write:
 AndroidMock.expect(view.getViewBundle()).andReturn(bundle1); AndroidMock.expect(view.getViewBundle()).andReturn(bundle2); 

Thirdly. After executing the test for imitation, you can check that all the necessary calls were made, but there were no necessary ones.
In my example, this is the string:
 AndroidMock.verify(view); 

She checks. that all expectations that were described before the line
 AndroidMock.replay(view); 

fulfilled.

It must be said that not only the fact of the method call is checked, but that the method is called with the correct arguments:
 AndroidMock.expect(view.getViewBundle(1)).andReturn(bundle1); AndroidMock.expect(view.getViewBundle(2)).andReturn(bundle2); 

In this case, different values ​​will be returned for different arguments, and during the check you can check that there was a call with the specified arguments.

When creating an imitation there are three methods:
AndroidMock.createNiceMock () - Creates a weak imitation. When checking will not be taken into account, extra methods were called. Only that declared methods will be checked. It is usually used when it is necessary to track that the correct methods were called, and if there were unnecessary ones, then it is not scary.
In those cases when imitation is needed only to create a class, they just use weak imitation and do not even cause verification after the test.

AndroidMock.createMock () - Creates a regular imitation. To verify that all necessary methods were called, and unnecessary methods were not called. This will check the number of calls. Usually one line in describing expectations is one call. But if you use the andStubReturn () method, then it is a lot of calls. You can also indicate a specific number of calls: andReturn (). Times (3);

AndroidMock.createStrcitMock () - Creates a strict simulation. in this case, even the order of method calls is checked.

Detailed reference information can be found on the AndroidMock and EasyMock project pages.

Unfortunately, AndroidMock has serious flaws.
It was announced that it could mimic built-in classes, such as MediaPlayer, but that was a long time ago and this does not work with the latest versions of the Android SDK.
You have to make your own interface, which contains all the necessary methods from the main class and how to make a View, which contains an instance of the real MediaPlayer and maps all calls from the interface to the methods of the real class.
In the tests, I imitate the created interface.

Therefore, I am going to look for another Framework for creating imitations.

Read in other articles.


- Introduction
- MVP and Unit tests. Jedi Way
- User Interface, Testing, AndroidMock
- Saving data. Domain Model, Repository, Singleton and BDD
- Server side implementation, RoboGuice, testing
- Small tasks, settings, logging, ProGuard

Source: https://habr.com/ru/post/131652/


All Articles