Which of the following is difficult for Android to perform unit tests?-part2

Source: Internet
Author: User

Which of the following is difficult for Android to perform unit tests?-part2

Where is it difficult to perform unit tests on Android? In part1, I will tell you that even code written by Google cannot be tested. Specifically, what I really tell you is: there is no way to perform a unit test in the onStop () method of SessionDetailActivity, and a detailed explanation of the cause and effect: As the pre-test status cannot be changed, we cannot complete assertions in the onStop () method; when testing in the onStop () method, the status cannot be completed after the test is obtained. At the end of the previous blog, I told you that it is precisely the features of Android SDK and the code templates officially recommended by Google that make unit testing so embarrassing, and I promise that I will explain all the reasons in detail in this blog post. Now let me fulfill my promise.

Before I begin my discussion, I will again: it is the standard Android application architecture that makes it so difficult to test Android applications. This is the core point of this series of blog posts. The significance of this blog post is: we try to give reasons to prove the necessity of restructuring Android applications, so that these Android applications do not need to be explicitly dependent on the Android SDK. At the same time, we are also trying to propose a robust application architecture to enhance the test performance of Android applications. You will learn the related Overview In this blog. Therefore, I will try to prove the core points of this blog.

As we all know, there is a standard architecture for developing Android applications. In the sample code and open source code, it is common that the application's business logic is executed in the component classes, activities, services, and Fragment of Android applications. I will follow this architecture for development. This blog post will discuss the following: if we follow this standard architecture for development, we are very likely to write code that cannot be tested. In the previous blog, I demonstrated that this problem is not accidental, it is precisely the standard Android application architecture that makes testing fragmented, and unit testing is almost impossible.

Traditional Android Application Architecture makes unit testing impossible

To demonstrate why the standard development architecture makes application components unable to be tested, you may wish to briefly review some conclusions in the previous blog with me. Unit Testing involves three steps: Preparation, testing, and assertion. To complete the preparation step, you need to change the pre-test status of the test code. In addition, to complete the assertion step of the unit test, we need to obtain the post-test status of the program.

After reviewing these knowledge points, you can start to get started. In some cases, dependency injection is the only way to change the pre-test status code, and the status of the Code after testing is accessible. I wrote an example that has nothing to do with Android:

    public class MathNerd {        private final mCalcCache;        private final mCalculator;        public MathNerd(CalculationCache calcCache, Calculator calculator) {            mCalcCache = calcCache;            mCalculator = calculator;        }        public void doIntenseCalculation(Calculation calculation, IntenseCalculationCompletedListener listener) {            if (!mCalcCache.contains(calculation)) {                mCalculator.doIntenseCalculationInBackground(listener);            } else {                Answer answer = mCalcCache.getAnswerFor(calculation);                listener.onCalculationCompleted(answer);            }        }    }

As shown above, dependency injection is indeed the only way To Perform unit tests on doIntenseCalculation (), because the doIntenseCalculation () method has no return value at all. In addition, the MathNerd class does not have any attribute that determines the status validity after the test. However, through dependency injection, we can use mCalcCache to obtain the post-test status in unit test.

    public void testCacheUpdate() {        //Arrange        CalculationCache calcCache = new CalculationCache();        Calculator calculator = new Calculator();        MathNerd mathNerd = new MathNerd(calcCache, calculator);        Calculation calcualation = new Calculation(e^2000);        //Act        mathNerd.doIntenseCalculationInBackground(calculation, null);        //some smelly Thread.sleep() code...        //Assert        calcCache.contains(calculation);    }

If we do this, I am sorry, it is impossible to implement a test unit for the MathNerd class. We will implement an integrated test to check whether the actual behavior of the MathNerd and the class are updated with CalcCache based on the value processed by the doIntenseCalculationInBackground () method.

In addition, dependency injection is actually the only way to verify the status after the test unit test. The injection verification method is called at the correct position:

    public void testCacheUpdate() {       //Arrange        CalculationCache calcCache = mock(CalculationCache.class);        when(calcCache.contains()).thenReturn(false);        Calculator calculator = mock(Calculator.class);        MathNerd mathNerd = new MathNerd(calcCache, calculator);        Calculation calculation = new Calculation(e^2000);        //Act        mathNerd.doIntenseCalculationInBackground(calculation, null);        //Assert should use calculator to perform calcluation because cache was empty        verify(calculator).doIntenseCalculationInBackground(any());    }

Many test instances involved in unit testing in the related classes of Android applications need one thing: dependency injection. But the problem arises: the core Android class holds the dependency that we cannot inject. For example, the SessionCalendarService started through SessionDetailActivity I mentioned last time is a good example:

    @Override    protected void onHandleIntent(Intent intent) {        final String action = intent.getAction();        Log.d(TAG, Received intent:  + action);        final ContentResolver resolver = getContentResolver();        boolean isAddEvent = false;        if (ACTION_ADD_SESSION_CALENDAR.equals(action)) {            isAddEvent = true;        } else if (ACTION_REMOVE_SESSION_CALENDAR.equals(action)) {            isAddEvent = false;        } else if (ACTION_UPDATE_ALL_SESSIONS_CALENDAR.equals(action) &&                PrefUtils.shouldSyncCalendar(this)) {            try {                getContentResolver().applyBatch(CalendarContract.AUTHORITY,                        processAllSessionsCalendar(resolver, getCalendarId(intent)));                sendBroadcast(new Intent(                        SessionCalendarService.ACTION_UPDATE_ALL_SESSIONS_CALENDAR_COMPLETED));            } catch (RemoteException e) {                LOGE(TAG, Error adding all sessions to Google Calendar, e);            } catch (OperationApplicationException e) {                LOGE(TAG, Error adding all sessions to Google Calendar, e);            }        } else if (ACTION_CLEAR_ALL_SESSIONS_CALENDAR.equals(action)) {            try {                getContentResolver().applyBatch(CalendarContract.AUTHORITY,                        processClearAllSessions(resolver, getCalendarId(intent)));            } catch (RemoteException e) {                LOGE(TAG, Error clearing all sessions from Google Calendar, e);            } catch (OperationApplicationException e) {                LOGE(TAG, Error clearing all sessions from Google Calendar, e);            }        } else {            return;        }       //...    }

SessionCalendarService depends on ContentResolver, and ContentResolver is a dependency that cannot be injected. Therefore, we cannot inject it in the onHandleIntent () method. The onHandleIntent () method does not return values. The SessionCalendarService class does not have accessible attributes that allow us to check the status after testing. To verify the status after the test, we can check whether the request data is inserted by querying ContentProvider, but we will not implement the test unit for SessionCalendarService in this way. Instead, we use an integrated test to test SessionCalendarService and calendar meeting data controlled by ContentProvider.

So if you put the business logic in the Android class, and the dependency of this class cannot be injected, then this part of the Code cannot be tested. Similar dependencies that cannot be injected are still available, such as FragmentManager of Activity and Fragment. Therefore, Google has encouraged us to use the standard Android application architecture model so far, teaching us to put the business logic in the component class of the application when developing the application, we vowed that this was good for us, and today we know the truth: It was such an architecture that made us write code that could not be tested.

The standard development model makes unit testing difficult

In some cases, the standard development mode makes unit testing of code very difficult. If we go back to the onStop () method in SessionDetailActivity mentioned in the previous blog, we can see that:

    @Override    public void onStop() {        super.onStop();        if (mInitStarred != mStarred) {            if (UIUtils.getCurrentTime(this) < mSessionStart) {                // Update Calendar event through the Calendar API on Android 4.0 or new versions.                Intent intent = null;                if (mStarred) {                    // Set up intent to add session to Calendar, if it doesn't exist already.                    intent = new Intent(SessionCalendarService.ACTION_ADD_SESSION_CALENDAR,                            mSessionUri);                    intent.putExtra(SessionCalendarService.EXTRA_SESSION_START,                            mSessionStart);                    intent.putExtra(SessionCalendarService.EXTRA_SESSION_END,                            mSessionEnd);                    intent.putExtra(SessionCalendarService.EXTRA_SESSION_ROOM, mRoomName);                    intent.putExtra(SessionCalendarService.EXTRA_SESSION_TITLE, mTitleString);                } else {                    // Set up intent to remove session from Calendar, if exists.                    intent = new Intent(SessionCalendarService.ACTION_REMOVE_SESSION_CALENDAR,                            mSessionUri);                    intent.putExtra(SessionCalendarService.EXTRA_SESSION_START,                            mSessionStart);                    intent.putExtra(SessionCalendarService.EXTRA_SESSION_END,                            mSessionEnd);                    intent.putExtra(SessionCalendarService.EXTRA_SESSION_TITLE, mTitleString);                }                intent.setClass(this, SessionCalendarService.class);                startService(intent);                if (mStarred) {                    setupNotification();                }            }        }    }

As you can see, the onStop () method does not let us know whether SessionCalendarService uses the correct parameter to start the accessible attributes. In addition, onStop () A method is a protected method, so that its return value cannot be modified. Therefore, the only way to access the status after the test is to check the status of the injection to the onStop () method.

In this way, we will notice that the code used to start SessionCalendarService in the onStop () method does not belong to a certain class. In other words, the dependencies injected in the onStop () method do not have the attributes used to check whether SessionCalendarService is in the status after the test unit test is started with the correct parameters. To propose a third way to turn the onStop () method into testable, we need something like this:

    @Override    public void onStop() {        super.onStop();        if (mInitStarred != mStarred) {            if (UIUtils.getCurrentTime(this) < mSessionStart) {                // Update Calendar event through the Calendar API on Android 4.0 or new versions.                Intent intent = null;                if (mStarred) {                    // Service launcher sets up intent to add session to Calendar                    mServiceLauncher.launchSessionCalendarService(SessionCalendarService.ACTION_ADD_SESSION_CALENDAR, mSessionUri,                                                                 mSessionStart, mSessionEnd, mRoomName, mTitleString);                } else {                    // Set up intent to remove session from Calendar, if exists.                    mServiceLauncher.launchSessionCalendarService(SessionCalendarService.ACTION_REMOVE_SESSION_CALENDAR, mSessionUri,                                                                mSessionStart, mSessionEnd, mTitleString);                }                if (mStarred) {                    setupNotification();                }            }        }    }

Although this is not the simplest way to refactor the onStop () method, if we write the business logic in the Activity according to the standard development method, and let the written code Perform unit testing, similar processing becomes necessary. Now let's think about how this refactoring method violates common sense: We didn't simply call the startService () method (startService () is a method of Context, we can even call the SessionDetailActivity method), but start the service by relying on the ServiceLauncher object of Context. As a subclass of Context, SesionDetailActivity also uses an object holding Context to start SessionCalendarService.

Unfortunately, even if we refactor the onStop () method as mentioned above, we still cannot guarantee that we can implement the test unit for the onStop () method. The problem is that ServiceLauncher is not injected, so we cannot inject ServiceLauncher, so that we can verify that the correct method is called during the test.

To inject ServiceLauncher, in addition to the mentioned above, it will become complicated because ServiceLauncher itself depends on Context, because Context is a non-packaged object. Therefore, you cannot simply inject ServiceLauncher by passing in the Intent used to start SessionDetailActivity. So in order to inject ServiceLauncher, you need to start your brain, or use injection libraries similar to Dagger handler. Now you should also find that in order for our code to perform unit tests, we really need to do a lot of complicated and tedious work, and, as I will discuss in the next blog, even if we use libraries such as Dagger for dependency injection, unit testing in the Activity is still suffering.

In order to enable the onStop () method to perform unit tests, the standard development method forces us to use an abnormal refactoring method, it also requires us to come up with a better reconstruction method based on the Intent-based dependency injection mechanism "or" Use third-party dependency injection for warehouse receiving ". The standard development method brings difficulties in writing testable code, just as it encourages us to write code that cannot be tested. This difficulty makes me think: standard Development Methods prevent us from writing testable code.

Conclusion

In the entire series of blog posts, I have been proposing this point: by reflecting on why unit tests in Android are so difficult, we will find the advantages of restructuring the application architecture, this eliminates the need to explicitly rely on the Android SDK for our applications. From this blog post, I believe you have enough reason to believe that getting rid of the Android SDK is a good offer.

I just put the business logic in the component class of the application and proved to everyone how difficult it is to perform unit tests on it. We can even say that it is impossible to perform unit tests on it. In the next blog, I suggest you delegate your business logic to a class that uses the correct dependency injection posture. If we think it is very troublesome to define these classes, we can leave them to the next level and make the dependencies of these classes become Android-independent interfaces. This step is crucial compared to the first step to enhance program testing, and completing the second step removes the need for Android-specific test tools (for example, Roboletric, Instrumented Tests) you can write down more efficient test units.

Note

There is no doubt that you should make ServiceLauncher A serialized object when passing in. But this is not a very robust solution, because it can be used only when you don't care about the performance impact of serialization.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.