Design patterns for Android development. Part 2 - MVP and Unit tests. Jedi Way

    At first, I just wanted to briefly tell you what MVP is, but it did not work out briefly. Therefore, I highlighted this piece in a separate article, which has little relation to Android, but is very important for understanding MVP and unit tests. The promised articles will not go anywhere.

    All developers know what unit tests are, but few people can use them so that their use reduces the development time of the program, but does not increase it. In their eyes, those who can do tasks faster and better with the help of tests look like Jedi who can crush a company of machine gunners with cabbage in their hands with their eyes closed.

    I’ll try to put you on the path of the Jedi and teach you to crumble piles of bugs into cabbage with closed eyes using unit tests and other technologies.


    What is Model View Presenter (MVP)


    From afar I’ll tell you what MVP is, why it is needed and what is the use of it.
    In any project, there comes a point when developers want to add unit tests to ensure that the latest changes have not broken everything that was done before the intermediate version is released.

    It seems to be clear what the test should do: it is necessary to check that after the user sees the form, fills in the fields with the correct data and clicks “Save”, in the handler of this button, the data from the form was checked and got into the database, or the user would receive an error message .
    All the examples that are in the documentation for different languages ​​are built on this simple principle. Therefore, this is a very common design decision.

    But here the first problem appears, because the logic for checking the correctness of the fields and saving them in the database lies in the handler of the "Save" button. How to test the click of the "Save" button on the form in the test? Indeed, in the test it is impossible to create an instance of the class “form”, most often because this class does not have a public constructor, in other words, the form is created by special classes that are not in the tests, and which, in turn, cannot be created either.

    MVP allows you to beautifully solve the mentioned problem. The bottom line is that you need to transfer all the code from the button handler to save it in another class, which can be easily created. This class is called presenter. And in the form, which is now called View or view, only one line will remain in the handler of the save button, which will call the method of the same name in presenter.
    Typically, a presenter is created in the view constructor and a reference to the view is passed to the presenter.

    Further in the test, we create an instance of presenter and simulate a call to the "save" method of presenter. Further, we expect that presenter will take the values ​​of the form fields from the view, check them and pass them to the database. It turns out that the presenter in the test still needs a form, and even the database in the form of some class for working with the database.

    We cannot create a form and class for working with the database in the test, but we can do the following feint with our ears: let the presenter, when created, receive references not to the view and the class for working with the database, to their interfaces. That is, you need to create interfaces, one for presentation, and another for the database. During normal work under these interfaces, the real presentation and the database will be hidden, and in tests we will make stub classes that will also support these interfaces. In the tests, we will create a stub for the view, which will return fixed values ​​for the form fields. A stub for the database will simply save the values ​​that came to it, so that later we can check these values.

    And now in the test:
    1. Create a class stub for presentation and specify what field values ​​it will return.
    2. Create a stub class for the database that simply saves the values ​​passed to it when the “SaveToDB” method was called
    3. Create a presenter and pass the created stubs to it.
    4. We call the presenter method "save"
    5. check that the necessary values ​​appeared in the DB stub, otherwise there is an error in the presenter.

    So why is MVP needed and what is the use of it.
    MVP is a design pattern that allows you to separate code with business logic for processing data from code that displays data on the form.
    The business logic in presenter is tested every time a new version is released. That allows us to say that there are no complex errors in the program.
    The code in the view has not been tested, but these errors are easily discovered by developers when they see that the "Save" button does nothing, and are easily fixed, because it is immediately clear that they forgot to call the presenter method in the button handler.

    The attentive reader at this place will exclaim, “Yes you are fucked up!” Previously, I had one class that fit in a single file and it was convenient to debug everything in it, but now I need to add the presenter class, interfaces, two stubs and the test itself! It will take me twice as long. If the bosses orders, then I will be doing a couple of tests after I write the code, but I will not be held responsible for breaking the deadlines ”

    “Calm down, O young Padawan”


    Yes, any pattern requires writing additional lines of code, you just need to understand how to benefit from it.

    Basically, developers do not use unit tests, because it seems to them that this is just a waste of additional time writing a test, and testers who benefit from less testing benefit from this. So saving time in a project can only be achieved due to the fact that testers work less and this time saving cannot be more than the time spent writing tests.

    So, in this way, unit tests cannot be used. They must be used so that they save time for the developer.

    Further I will tell you how to organize the development process using tests, so that it is of benefit to the developer.

    As an example, we take the same form where you need to enter data, click the "Save" button, check the data, if there are errors, then show them to the user, if not, then send them to the database.

    Creating a view and preset for a presenter


    First of all, you need to create a form (view), in the case of Android, this is the heir of the Activity class. It is necessary to place controls on it to enter values, display an error message and the "Save" button with a handler.

    Next, you need to create an interface for the presentation. The interface is needed so that presenter can output data to the form and take data from the form, so the interface should have methods like setName (string name) and string getName (). Naturally, the view should implement this interface and in methods should simply shift the value from the argument to the Text property of the control used or vice versa in the case of the getName method.

    Next, you need to create a presenter, which receives a link to the interface instance at the input. For now, in the presenter constructor, we simply put arbitrary values ​​on the form to verify that the presenter has full access to the form.
    We also create the “save” method, in which we get the current values ​​from the fields of the view in local variables and put Break Point in it to make sure that this method is called from the view.

    In the view constructor, create an instance of presenter and give it a link to this view. In the view in the method, the handler of the "Save" button simply puts a call to the corresponding method in presenter.

    Here is an example view
    public class View extends Activity implements IView
    {
    	Presenter presenter;
    	public View(Bundle savedInstanceState)
    	{
    		presenter = new Presenter(this);
    	}
    	public void setName(String name)
    	{
    		tvName.setText(name);
    	}
    	public void getName()
    	{
    		return tvName.getText();
    	}
    	public void onSave(View v)
    	{
    		presenter.onSave();
    	}
    }
    

    Here is an example of a presenter

    public class presenter
    {
    	IView view;
    	public presenter(IView view)
    	{
    		this.view = view;
    		view.setName("example name");
    	}
    	public onSave()
    	{
    		string name = view.getName();
    	}
    }
    

    The code for the first stage is ready.
    You need to run the program in the debugger and make sure that when you open the form the correct name is displayed, and if you enter a new name and click "Save", then presenter will get the correct value.

    At the first stage, there are no unit tests and there is no particular time saving either, but the costs are not large.

    "Close your eyes, O young Padawan."


    Now the fun begins. The fact is that with further writing of the code, we do not need to run the application to watch how the form works. We will develop and debug all code in tests. We will write the code as if with “eyes closed”, like real Jedi.

    The secret is to add presenter in small pieces and add a piece of dough to test this piece.

    Fill presenter functionality. upon creation, he must read the name from the database and pass it into the view. And when saving, get the name from the presentation and save it in the database.

    public class presenter
    {
    	IView view;
    	IDataBase dataBase;
    	int Id;
    	public presenter(IView view, IDataBase dataBase)
    	{
    		this.view = view;
    		this.dataBase = dataDase;
    		id = view.getId();
    		string name = dataBase.loadFromDB(id);
    		view.setName(name);
    	}
    	public onSave()
    	{
    		string name = view.getName();
    		dataBase.saveToDB(id, name);
    	}
    }
    

    It is clear that at the same time we need to add a new method to the interface and view to get the ID, but for now we will not even “open our eyes”, that is, run the application to check the presentation. We will do this later, and if there is an error, we will fix it in a second.

    So we wrote a piece of the presentation and now write a piece of test.

    Those stubs about which I wrote in the first part, it is not necessary to create in the form of classes that support the interface. If the various Mock Framework, which allow you to create in three lines, create an instance of a class that supports the specified interface and even returns the desired values ​​when calling its methods. These stubs are called imitations (translation of the word Mock), because they are much more sophisticated than just stubs.

    Test Method Example

    public void testOpenAndSave()
    {
    	IView view = createMock(IView.class);	//создаем имитацию для представления
    	IDataBase dataBase = CreateMock(IDataBase.class);//создаем имитацию для БД
    	expect(view.getId()).andReturn(1); //Когда presenter спросит что прочитать из базы вернем id 1
    	expect(dataBase.LoadFromDB(1)).andReturn("source name"); //Когда presenter попытается загрузить название, вернем "source name"
    	expect(view.setName("source name")); //ожидаем что правильная строка попадает на форму
    	expect(view.getName()).andReturn("destination name"); //Когда presenter  при сохранении спросит, что ввел пользователь, вернем "destination name".
    	expect(dataBase.SaveToDB(1, "destination name")); //Ожидаем, что в БД уйдет именно это название.
    	Presenter presenter = new Presenter(view, dataBase); //Передаем в presenter имитации для представления и базы данных
    	presenter.Save();//Имитируем нажатие кнопки "Сохранить";
    	//проверим, что все нужные методы были вызваны с нужными аргументами
    	verify(view);
    	verify(dataBase);
    }
    

    Just do not groan that the code in the test is as much as in the presenter. It's worth it.

    We run the test and see that everything is fine, or do so that everything becomes good.

    With this development process, the test is not something alien to the code. A test is a piece of code that a developer must issue when implementing a task. A test is needed to make sure that the written code works correctly.

    Thinking over the code in presenter, you need to immediately think over the code in the test; these are the yin and yang of the finished task, the parts are inseparable from each other.

    And here is the profit


    And now I will show where the profit is in the form of reducing development time and improving quality.

    Profit appears in the further development of the presenter.

    Next, you need to add validation from the form to the save method. And in the test, you need to check that the correct values ​​are stored in the database, and an error message is displayed on incorrect ones.

    To do this, add the necessary code to the presenter in order to verify the name is correct and if it is incorrect, then call the setErrorText () method, and do not call the save to the database.

    Next, just copy the test and edit the simulation for presentation. It should now return the wrong name when calling getName () and wait for the setErrorText () method to be called with the correct argument, thus checking that the error message is correct. And the simulation for the database should check that the SaveToDB method has never been called, or rather, called 0 times.

    We run all the tests we see, the new test is working fine. But the past does not pass. We beat ourselves on the forehead for a stupid mistake and quickly correct it.
    And now both tests work.

    If it were not for our first test, then the testers would have found an error with saving the correct lines and as a result, the time to fix it would be several times longer than using unit tests. It would be necessary to again open the right place in the code and remember how the algorithm works and make corrections. And then, since there are no tests, check all other test cases yourself to make sure that fixing this error did not lead to another.

    When the algorithms are complex and you have to check a bunch of test cases, then saving time becomes much more noticeable. Because without tests, you also need to spend time launching the entire application, then go through several windows to get to your own, then play the test case, and this can take a lot of effort.

    But we have tests. Therefore, we can easily simulate various test cases and fix bugs in batches, because all past test cases will also be checked.

    As a result, we got a high-quality code, and time was saved due to the fact that you do not have to distract from the errors that the testers found, and then also to check for the longest time that all other test cases did not break after fixing the error.

    And so, after a few days, when the entire presenter is ready and tested for all cases, we launch the application and are surprised to see that it works right right for all test cases. And we feel like a real Jedi.

    The Jedi is not easy to become.


    Yes. A true Jedi should know many more tricks for writing tests so that testing support, when changing requirements, does not gobble up all the benefits that came from the initial development.

    Actually, copying tests is just as bad as copying code in an application. We need to do a test that works with the data structure that describes the test case. This data structure contains input and expected output or expected errors.
    in this case, a new test is a call to the same method simply with a different data structure. And the test should be developed so that it can process all structures.

    I believe on the Internet you can find many such tricks that will allow you to become a real Jedi Master.

    The main thing is that you have already embarked on the path of the Jedi and know how to follow it :)

    Read in other articles


    - Introduction
    - MVP and Unit tests. Jedi Path
    - User Interface, Testing, AndroidMock
    - Saving Data. Domain Model, Repository, Singleton and BDD
    - Server Part Implementation, RoboGuice, Testing
    - Small Tasks, Settings, Logging, ProGuard

    Also popular now: