It's hard to do unit testing on ANDROID-PART2

Source: Internet
Author: User
<span id="Label3"><p><p>Tag: Android Unit Unit test</p></p> <blockquote> <blockquote> <ul> <li>Original Link: Why Android Unit testing are so hard</li> <li>Original Author: Matthew Dupree</li> <li>Development technology Front www.devtf.cn</li> <li>Translator: chaossss</li> <li>Reviewer: Tiiime</li> <li>Status: Complete</li> </ul> </blockquote> </blockquote><p><p>It's hard to do unit testing on android-part1, I use dry goods to tell You: even Google Daniel wrote the code can not be Tested. To be exact, What I really tell you is that there is no way to unit test in Sessiondetailactivity's onStop () method, and explain in detail the causal: we cannot complete the assertion in the OnStop () method due to the inability to change the predictive state; When testing in the OnStop () method, it is not possible to obtain a post-test State. At the end of the previous blog post, i said: it is some of the features of the Android SDK, and Google's official recommended code template to make unit testing in such an awkward situation, and I promise to explain in this blog in detail the various causes, then let me now to honor my Promise.</p></p><p><p>Before I start, I say again: it's the core of this series of blogs that the standard Android application architecture makes testing Android apps so Difficult. The point of this blog post is that we're trying to justify the need to refactor Android apps so that these Android apps don't have to rely explicitly on the Android SDK, and We're also trying to put forward a robust application architecture that enhances the testing of Android apps, You will find an overview of the relevant it in this Article. so, I'm going to try to prove the core point of this blog Post.</p></p><p><p>As we all know, the development of Android applications has a standard architecture, in the sample code and open source code is very common to the application of business logic is placed in the Android application component class, activity,service,fragment Execution. And then I'm going to follow this architecture for Development. And this blog post is to discuss: if we follow this standard architecture for development, it is very likely to write down the code can not be tested, I also demonstrated in the previous it that the problem is not accidental, it is the standard Android application architecture to make testing fragmented, unit testing is almost impossible.</p></p>Traditional Android Application architecture makes unit testing impossible<p><p>To start demonstrating why the standard development architecture makes application components impossible to test, you might as well review some of the conclusions of the previous blog post with Me briefly. Unit testing consists of three steps: prepare, test, assert. In order to complete the preparation steps, we need to change the test Code's predictive state, in addition, in order to complete the unit test assertion step, We need to obtain the Program's post-test State.</p></p><p><p>After reviewing these points of knowledge, you can start to go to the point of the Ha. In some cases, dependency injection is the only way to implement the ability to change the predictive status code, and the post-test state of the code is also accessible. I wrote an example that was completely unrelated to Android:</p></p><pre class="prettyprint"><code class="language-java hljs "> <span class="hljs-keyword"><span class="hljs-keyword"></span> public</span> <span class="hljs-class"><span class="hljs-class"> <span class="hljs-keyword">class</span> <span class="hljs-title">mathnerd</span> {</span></span> <span class="hljs-keyword"><span class="hljs-keyword">Private</span></span> <span class="hljs-keyword"><span class="hljs-keyword">Final</span></span>mcalccache;<span class="hljs-keyword"><span class="hljs-keyword">Private</span></span> <span class="hljs-keyword"><span class="hljs-keyword">Final</span></span>mcalculator;<span class="hljs-keyword"><span class="hljs-keyword"></span> public</span> <span class="hljs-title"><span class="hljs-title">Mathnerd</span></span>(calculationcache calccache, Calculator Calculator) {mcalccache = calccache; Mcalculator = calculator; }<span class="hljs-keyword"><span class="hljs-keyword"></span> public</span> <span class="hljs-keyword"><span class="hljs-keyword">void</span></span> <span class="hljs-title"><span class="hljs-title">dointensecalculation</span></span>(calculation calculation, Intensecalculationcompletedlistener Listener) {<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(!mcalccache.contains (CALCULATION)) {mcalculator.dointensecalculationinbackground (listener); }<span class="hljs-keyword"><span class="hljs-keyword">Else</span></span>{Answer Answer = mcalccache.getanswerfor (calculation); Listener.oncalculationcompleted (answer); } } }</code></pre><p><p> As shown above, dependency injection is really the only way to unit test dointensecalculation () because the Dointensecalculation () method has no return value at All. In addition, there is no attribute in the Mathnerd class to determine the validity of the post-test State. But with dependency injection, we can get the post-test state in the unit test through Mcalccache. </p></p><pre class="prettyprint"><pre class="prettyprint"><code class="language-java hljs"> <span class="hljs-keyword">public </span> <span class="hljs-keyword">void </span> <span class="hljs-title">testcacheupdate </span> () {<span class="hljs-comment">// Arrange </span> Calculationcache calccache = <span class="hljs-keyword">new </span> calculationcache (); Calculator Calculator = <span class="hljs-keyword">new </span> Calculator (); Mathnerd mathnerd = <span class="hljs-keyword">new </span> mathnerd (calccache, calculator); Calculation calcualation = <span class="hljs-keyword">new </span> calculation (<span class="hljs-string"> "e^2000" </span>); <span class="hljs-comment">//act </span> mathnerd.dointensecalculationinbackground (calculation, <span class=" Hljs-keyword ">null </span>); <span class="hljs-comment">//some smelly thread.sleep () code ... </span> <span class="hljs-comment">//assert </span> calccache.contains (calculation); }</code></pre></pre><p><p>If we do this, unfortunately, I am afraid there is no way to implement a test unit for the Mathnerd class. We will implement a consolidation test that checks the actual behavior of the mathnerd and whether the class updates the Calccache based on the values processed by the Dointensecalculationinbackground () method.</p></p><p><p>In addition, dependency injection is actually the only way to verify the state of a test unit after Testing. We are called in the right place by injecting a validation method:</p></p><pre class="prettyprint"><code class="language-java hljs "><code class="language-java hljs"> <span class="hljs-keyword">public </span> <span class="hljs-keyword">void </span> <span class="hljs-title">testcacheupdate </span> () {<span class="hljs-comment">//arrange </span> CALCU Lationcache Calccache = mock (calculationcache.class); When (calccache.contains ()). thenreturn (<span class="hljs-keyword">false </span>); Calculator Calculator = mock (calculator.class); Mathnerd mathnerd = <span class="hljs-keyword">new </span> mathnerd (calccache, calculator); Calculation calculation = <span class="hljs-keyword">new </span> calculation (<span class="hljs-string"> "e^2000" </span>); <span class="hljs-comment">//act </span> mathnerd.dointensecalculationinbackground (calculation, <span class=" Hljs-keyword ">null </span>); <span class="hljs-comment">//assert should use calculator to perform calcluation because cache is empty </span> ver Ify (calculator). dointensecalculationinbackground (any ()); }</code> </code></pre><p><p>Many of the test instances involved in unit testing in related classes in Android apps require one thing: dependency Injection. But Here's the problem: the core Android class holds the dependency we can't inject. For example, the last time I mentioned the Sessioncalendarservice that started with sessiondetailactivity is a good example:</p></p><pre class="prettyprint"><code class="language-java hljs "> <span class="hljs-annotation"><span class="hljs-annotation">@Override</span></span> <span class="hljs-keyword"><span class="hljs-keyword">protected</span></span> <span class="hljs-keyword"><span class="hljs-keyword">void</span></span> <span class="hljs-title"><span class="hljs-title">onhandleintent</span></span>(Intent Intent) {<span class="hljs-keyword"><span class="hljs-keyword">Final</span></span>String action = intent.getaction (); LOG.D (TAG,<span class="hljs-string"><span class="hljs-string">"Received intent:"</span></span>+ action);<span class="hljs-keyword"><span class="hljs-keyword">Final</span></span>Contentresolver resolver = Getcontentresolver ();<span class="hljs-keyword"><span class="hljs-keyword">Boolean</span></span>Isaddevent =<span class="hljs-keyword"><span class="hljs-keyword">false</span></span>;<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(action_add_session_calendar.equals (ACTION)) {isaddevent =<span class="hljs-keyword"><span class="hljs-keyword">true</span></span>; }<span class="hljs-keyword"><span class="hljs-keyword">Else</span></span> <span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(action_remove_session_calendar.equals (ACTION)) {isaddevent =<span class="hljs-keyword"><span class="hljs-keyword">false</span></span>; }<span class="hljs-keyword"><span class="hljs-keyword">Else</span></span> <span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(action_update_all_sessions_calendar.equals (ACTION) && Prefutils.shouldsynccalendar (<span class="hljs-keyword"><span class="hljs-keyword"></span> this</span>)) {<span class="hljs-keyword"><span class="hljs-keyword">Try</span></span>{getcontentresolver (). Applybatch (calendarcontract.authority, Processallsessionscale Ndar (resolver, Getcalendarid (intent))); Sendbroadcast (<span class="hljs-keyword"><span class="hljs-keyword">New</span></span>Intent (sessioncalendarservice.action_update_all_sessions_calendar_completed)); }<span class="hljs-keyword"><span class="hljs-keyword">Catch</span></span>(remoteexception E) {LOGE (TAG,<span class="hljs-string"><span class="hljs-string">"Error Adding all sessions to Google Calendar"</span></span>, e); }<span class="hljs-keyword"><span class="hljs-keyword">Catch</span></span>(operationapplicationexception E) {LOGE (TAG,<span class="hljs-string"><span class="hljs-string">"Error Adding all sessions to Google Calendar"</span></span>, e); } }<span class="hljs-keyword"><span class="hljs-keyword">Else</span></span> <span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(action_clear_all_sessions_calendar.equals (ACTION)) {<span class="hljs-keyword"><span class="hljs-keyword">Try</span></span>{getcontentresolver (). Applybatch (calendarcontract.authority, processclearallsession s (resolver, Getcalendarid (intent))); }<span class="hljs-keyword"><span class="hljs-keyword">Catch</span></span>(remoteexception E) {LOGE (TAG,<span class="hljs-string"><span class="hljs-string">"Error clearing all sessions from Google Calendar"</span></span>, e); }<span class="hljs-keyword"><span class="hljs-keyword">Catch</span></span>(operationapplicationexception E) {LOGE (TAG,<span class="hljs-string"><span class="hljs-string">"Error clearing all sessions from Google Calendar"</span></span>, e); } }<span class="hljs-keyword"><span class="hljs-keyword">Else</span></span>{<span class="hljs-keyword"><span class="hljs-keyword">return</span></span>; }<span class="hljs-comment"><span class="hljs-comment">//...</span></span>}</code></pre><p><p>Sessioncalendarservice relies on contentresolver, and Contentresolver is a dependency that cannot be injected, so if there is no way to inject it in the onhandleintent () method. The Onhandleintent () method has no return value, and there is no accessible property in the Sessioncalendarservice class that allows us to check the state after the Test. To verify the post-test state, we can check that the request data is inserted by querying contentprovider, but we will not implement the test unit in this way for sessioncalendarservice. instead, we're using an integrated test that tests Sessioncalendarservice and contentprovider-controlled calendar meeting Data.</p></p><p><p>So if you put the business logic in the Android class, and this kind of dependency can not be injected, then this part of the code will not be able to do unit testing. Similar dependencies that cannot be injected include, for example: Activity and Fragment fragmentmanager. So so far, Google has been encouraging us to use the standard Android application Architecture model, which teaches us to put business logic in the Application's component class when developing an application, pledging to say it's good for us, And today we know the truth: it is this architecture that allows us to write code that cannot be Tested.</p></p>Standard development model makes unit testing difficult<p><p>In some cases, the standard development pattern makes unit testing of code very DIFFICULT. If we go back to the OnStop () method in the sessiondetailactivity mentioned in the previous blog post, you can see:</p></p><pre class="prettyprint"><code class="language-java hljs "> <span class="hljs-annotation"><span class="hljs-annotation">@Override</span></span> <span class="hljs-keyword"><span class="hljs-keyword"></span> public</span> <span class="hljs-keyword"><span class="hljs-keyword">void</span></span> <span class="hljs-title"><span class="hljs-title">OnStop</span></span>() {<span class="hljs-keyword"><span class="hljs-keyword">Super</span></span>. OnStop ();<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(minitstarred! = Mstarred) {<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(uiutils.getcurrenttime (<span class="hljs-keyword"><span class="hljs-keyword"></span> this</span>) < Msessionstart) {<span class="hljs-comment"><span class="hljs-comment">//Update Calendar Event through the calendar API on Android 4.0 or new Versions.</span></span>Intent Intent =<span class="hljs-keyword"><span class="hljs-keyword">NULL</span></span>;<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(mstarred) {<span class="hljs-comment"><span class="hljs-comment">//Set up Intent to add session to Calendar, if it doesn ' t exist already.</span></span>Intent =<span class="hljs-keyword"><span class="hljs-keyword">New</span></span>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); }<span class="hljs-keyword"><span class="hljs-keyword">Else</span></span>{<span class="hljs-comment"><span class="hljs-comment">//Set up intent-remove session from Calendar, if Exists.</span></span>Intent =<span class="hljs-keyword"><span class="hljs-keyword">New</span></span>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 (<span class="hljs-keyword"><span class="hljs-keyword"></span> this</span>, sessioncalendarservice.class); StartService (intent);<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(mstarred) {setupnotification (); } } } }</code></pre><p><p>As you can see, there is nothing in the OnStop () method that lets us know if Sessioncalendarservice is starting with the correct arguments, and the OnStop () method is a protected method so that its return value cannot be modified. therefore, the only way we can access the post-test state is to check the injected state injected into the onStop () method.</p></p><p><p>In this way, we will notice that the code used to start Sessioncalendarservice in the OnStop () method does not belong to a class. In other words, the dependency injected in the OnStop () method does not have a property that checks the state of the test unit after the test Sessioncalendarservice is correctly started with the correct parameters. To suggest a third way to make the OnStop () method testable, We need something like this:</p></p><pre class="prettyprint"><code class=" hljs java"> <span class="hljs-annotation"><span class="hljs-annotation">@Override</span></span> <span class="hljs-keyword"><span class="hljs-keyword"></span> public</span> <span class="hljs-keyword"><span class="hljs-keyword">void</span></span> <span class="hljs-title"><span class="hljs-title">OnStop</span></span>() {<span class="hljs-keyword"><span class="hljs-keyword">Super</span></span>. OnStop ();<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(minitstarred! = Mstarred) {<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(uiutils.getcurrenttime (<span class="hljs-keyword"><span class="hljs-keyword"></span> this</span>) < Msessionstart) {<span class="hljs-comment"><span class="hljs-comment">//Update Calendar Event through the calendar API on Android 4.0 or new Versions.</span></span>Intent Intent =<span class="hljs-keyword"><span class="hljs-keyword">NULL</span></span>;<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(mstarred) {<span class="hljs-comment"><span class="hljs-comment">//Service Launcher sets up intent to add session to Calendar</span></span>Mservicelauncher.launchsessioncalendarservice (sessioncalendarservice.action_add_session_calendar, mSessionUri, msessionstart, msessionend, mroomname, mtitlestring); }<span class="hljs-keyword"><span class="hljs-keyword">Else</span></span>{<span class="hljs-comment"><span class="hljs-comment">//Set up intent-remove session from Calendar, if Exists.</span></span>Mservicelauncher.launchsessioncalendarservice (sessioncalendarservice.action_remove_session_calendar, msessionuri, msessionstart, msessionend, mtitlestring); }<span class="hljs-keyword"><span class="hljs-keyword">if</span></span>(mstarred) {setupnotification (); } } } }</code></pre><p><p>While 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 do the unit test, the similar processing becomes necessary. Now consider how counterintuitive this refactoring is: we don't simply call the StartService () method (startservice () is a method of the Context, and we can even say that the Sessiondetailactivity method is Called) , instead, the service is started by relying on the Servicelauncher object of the Context. Sesiondetailactivity as a sub-class of the context will also use a Context-holding object to start Sessioncalendarservice.</p></p><p><p>unfortunately, even if we refactor the OnStop () method as described above, we still cannot guarantee that the test unit can be implemented for the OnStop () method. The problem is that Servicelauncher is not injected, so we can't inject servicelauncher, so we can verify that the correct method was called during the Test.</p></p><p><p>To inject servicelauncher, in addition to what has just been mentioned, the servicelauncher itself relies on the context to become complex because the context is a non-packaged object. therefore, you cannot simply inject servicelauncher by passing it into the Intent that is used to start Sessiondetailactivity. So in order to inject servicelauncher, you need to start your little brain, or use a Dagger1-like Note. Now you should also find that in order for our code to be unit tested, we do need to do a lot of complicated and tedious work, and, as I will discuss in the next post, even if we use libraries like Dagger for dependency injection, the Activity In-unit testing is still an ordeal.</p></p><p><p>In order for the OnStop () method to be unit tested, the standard development approach forces us to use an anti-common refactoring approach and requires that we come up with a better refactoring method based on the Intent-based dependency injection mechanism or use a third-party dependency injection library. The standard development approach is the difficulty of writing testable code, like encouraging us to write code that cannot be tested, which makes me think that the standard development approach prevents us from writing testable Code.</p></p>Conclusion<p><p>Throughout the series, I have been making the point that by rethinking why unit testing in Android is so difficult, it will help us discover the benefits of refactoring the application architecture so that our applications don't have to rely explicitly on the Android SDK. This blog post discusses here, and I believe there are plenty of reasons to believe that getting rid of the Android SDK is probably a good idea.</p></p><p><p>I just put the business logic in the Application's component class and showed you how difficult it is to unit-test it, and even we can say that it's impossible to unit-test it. In the next blog post, I will recommend that you delegate business logic to classes that use the correct dependency injection Posture. If we find it troublesome to define these classes, we can get back to the second and make the dependencies of these classes an interface that is not related to Android. This step is crucial compared to the first step in enhancing the testing of the program, and completing the second step allows us to write more efficient test units without the need for Android-specific test tools (for example: roboletric,instrumented Tests).</p></p><p><p><strong>Note</strong></p></p> <ol> <ol> <li>There is no doubt that when you pass in servicelauncher, you should make him a serialized object. But this is not a particularly robust solution, because you can only use this approach if you don't care about the performance impact of Serialization.</li> </ol> </ol> <p><p>It's hard to do unit testing on ANDROID-PART2</p></p></span>

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.