At first, I just wanted to briefly tell you what MVP is, but it did not work out briefly. Therefore, I have highlighted this piece in a separate article, which has little relevance to Android, but is very important for understanding MVP and unit tests. The promised articles will not go anywhere.
All developers know what modular tests are, but few can use them so that their use shortens the development time of the program, and does not increase it. In their eyes, those who, with the help of tests, can do tasks faster and even more qualitatively, look like Jedi, who can, with the sword in their hands with their eyes closed, crumble a company of machine gunners into the cabbage.
I will try to put you on the path of the Jedi and teach you to crush heaps of bugs into cabbages with unit tests and other technologies with closed eyes.
')
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 time when developers want to add unit tests to ensure that the latest changes did not break everything that was done before releasing the intermediate version.
It seems clear what the test should do: it is necessary to check that after the user saw the form, filled in the fields with the correct data and clicked "Save", in the handler of this button, the data from the form was checked and entered into the database, or the user would get an error message .
All the examples that are in the documentation for different languages are built precisely on this simple principle. Therefore, this is a very common design solution.
But here the first problem appears, since the logic of checking the correctness of the fields and saving them in the database lies in the handler of the “Save” button. How in the test to check the button "Save" on the form? Indeed, a test cannot create an instance of the “form” class, 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 to another class, which can be easily created. This class calls presenter. And in the form, which is now called View or View, in the “save” button handler there will be only one line, which will call the method of the same name in the presenter.
Typically, a presenter is created in the presentation designer and the presenter is passed a link to the presentation.
Further in the test, we create an instance of the presenter and simulate a call to the “save” method of the presenter. Next, we expect the presenter to 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 a database in the form of a class to work with the database.
We cannot create a form and a class for working with a database, but we can make the following feint with our ears: let the presenter, when created, receive links not to the presentation and class to work with the database, to their interfaces. That is, you need to create interfaces, one for the presentation and the other for the database. In normal operation, these interfaces will hide the real representation and database, and in the tests we will make classes stubs, which will also support these interfaces. In tests, we will create a stub for the view that 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 could check these values.
And now in the test:
1. Create a stub class for the view and prescribe what field values it will return.
2. Create a stub class for the database, which simply saves the values passed to it when you call the SaveToDB method
3. We create presenter and we transfer to it the created stubs.
4. Call the “save” method at the presenter
5. we check that the necessary values appeared in the database stub, otherwise there was an error in the presenter.
So why do you need MVP and what is the use of it.
MVP is a design pattern that allows you to separate the code with business logic for data processing from the code with the data displayed on the form.
The business logic in the presenter is tested every time a new version is released. That allows you to say that there are no complex errors in the program.
The code in the view is not tested, but these errors are easily detected by the developers when they see that the “Save” button does nothing and are easily corrected, because it is immediately clear that they forgot to call the presenter method in the button handler.
An attentive reader will exclaim at this place “Yes, you fucked up! Previously, I had one class that fit in one file and it was convenient to debug everything in it, but now I need to add a presenter class, interfaces, two stubs, and I also have to write the test myself! It will take me twice as long. If the authorities order, then I will do a couple of tests after I write the code, but I disclaim responsibility for disrupting 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.
Most developers do not use unit tests, because it seems to them that this is only a waste of time to write a test, and testers who are less likely to test benefit from this. So, saving time in a project can only come about because testers work less and this time saving can not be more than the time spent writing tests.
So, in this way, unit tests can not be used. They must be used so that they save time for the developer.
Then I will tell you how to organize the development process using tests, so that it benefits the developer.
As an example, 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, send them to the database.
Creating a presentation and preparation for the presenter
The first step is to create a form (view), in the case of Android it is the heir of the Activity class. It is necessary to place on it controls for entering values, displaying 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 the 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 the methods it should simply shift the value from the argument to the Text property of the used control or vice versa in the case of the getName method.
Next you need to create a presenter, which at the entrance receives a link to an instance of the interface. For now, in the presenter's constructor, we simply write arbitrary values on the form to check that the presenter has full access to the form.
We will also create a “save” method, in which we will get the current values from the representation fields into local variables and put a Break Point in it to make sure that this method is called from the representation.
In the view designer, create an instance of the presenter and give it a link to this view. In the view in the handler method of the “Save” button, simply write down the call to the corresponding method in the 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 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", the presenter will receive the correct value.
At the first stage there are no unit tests and there is no special time saving either, but the costs are not great either.
"Close your eyes, oh young Padawan"
Now the fun begins. The fact is that with the further writing of the code, we do not need to launch the application in order to see how the form works. We will develop and debug all code in tests. We will write code as if with closed eyes, like real Jedi.
The secret is that you have to add presenter in small pieces and to check this piece add a piece of dough.
Let's fill the presenter with the functionality. when creating it, it must read the name from the database and pass it to 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 in this case it is necessary to add a new method to the interface and to the presentation to get the ID, but for now we will not even “open our eyes”, that is, launch 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 we write a piece of dough.
Those stubs, about which I wrote in the first part, do not necessarily create in the form of classes that support the interface. If various Mock Framework-i, which allow for three lines to 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 moan that the code in the test is the same as in the presenter. It's worth it.
We start the test and see that everything is fine, or we are doing it so that everything becomes good.
In this development process, the test is not foreign to the code. A test is a piece of code that a developer must issue when implementing a task. The test is needed to make sure that the written code works correctly.
Thinking through the code in the presenter, it is necessary to immediately think through the code in the test, it is the yin and yang of the finished task, the parts 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 after further development of the presenter.
Next you need to add data verification from the form to the “save” method. And in the test it is necessary to check that the correct values are stored in the database, and an incorrect error message is displayed.
To do this, add the necessary code to the presenter to check the correctness of the name and if it is incorrect, then call the setErrorText () method, and do not call saving to the database.
Next, just copy the test and edit the simulation for the presentation. It should now return the wrong name when calling getName () and wait for the call to the setErrorText () method with the correct argument, thus it will be checked that the error message is correct. And the simulation for the database should check that the SaveToDB method has never been called, more precisely, it was called 0 times.
We start all tests we see, the new test works fine. But the past does not pass. Beat your forehead for a stupid mistake and quickly correct it.
And now both tests work.
If it were not for our first test, the testers would have found an error in preserving the correct lines and, as a result, the time to correct it would be several times longer than using unit tests. It would be necessary to again open the right place in the code and recall how the algorithm works and make corrections. And then, since there are no tests, check all other test cases yourself to make sure that correcting 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 reproduce the test case, and this may require a lot of effort.
But we have tests. Therefore, we can easily imitate 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 is saved due to the fact that we don’t have to be distracted by the errors that testers found, and then also check for a long time that all other test cases did not break after correcting 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 for all test cases right away. And we feel like a real Jedi.
Jedi become not easy.
Yes. A real Jedi should know many more techniques for writing tests so that test support, when requirements change, does not devour all the benefits that came with the initial development.
In fact, copying tests is just as bad as copying code in an application. It is necessary to do a test that works with a data structure describing a test case. This data structure contains input data and expected output data or expected errors.
In this case, a new test is a call to the same method simply with a different data structure. A test must be developed so that it can handle all the structures.
I guess 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 taken the path of the Jedi and know how to follow it :)
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