Android unit testing - several important issues

Android unit testing - several important issues

[[173976]]

Original link: http://www.jianshu.com/p/f5d197a4d83a

Preface

I haven’t written an article for a month. I was planning a National Day trip in September and traveled for 14 days before and after the National Day, so I didn’t have time to write, haha.

Let’s get back to the topic. The previous article “Android Unit Testing - How to Start?” introduced several unit testing frameworks, basic usage of Junit & Mockito, dependency isolation & Mock concepts. This article mainly answers several important questions in unit testing.

In the unit testing discussion WeChat group, many new friends have similar questions. After a few of us veterans answered them again and again (shamelessly counting myself in ^_^), I got a little impatient and waited for other students to answer them... In fact, the questions everyone asked are all about "dependency issues", JVM dependency or Android dependency? What should I do if an error occurs when using native methods? How to solve static methods?

So, the author decided to write an article specifically to explain these issues.

  • How to solve Android dependencies?
  • Isolating Native Methods
  • Resolving internal new objects
  • Static methods
  • RxJava asynchronous to synchronous

1. How to solve Android dependencies?

Xiaobai: "TextUtils is used in Presenter, but when running junit, an error message appears saying 'java.lang.RuntimeException: Method isEmpty in android.text.TextUtils not mocked'... Should I use robolectric?"

Don't worry, it's not time for robolectric to appear yet!

Since junit runs on jvm, and jdk does not have android source code, classes such as TextUtils in android sdk cannot be referenced when running junit. Since jdk does not have it, we can add it ourselves!

In the test/java directory, create the android.text.TextUtils class

  1. package android.text;
  2.  
  3. public class TextUtils {
  4.  
  5. public   static boolean isEmpty(CharSequence str) {
  6. if (str == null || str.equals( "" )) {
  7. return   true ;
  8. }
  9. return   false ;
  10. }
  11. }

The key is to create a TextUtils with the same package name, class name, and method name. Note that it is not created under main/java, otherwise it will prompt Duplicate class found in the file... The unit test runs properly:

The principle is very simple. When the jvm is running, it will look for the android.text.TextUtils class, and then find the isEmpty method to execute. Students who have learned java reflection know that as long as you know the package name and class name, you can get the Class. If you know the name of a method in the class, you can get the Method and execute it. The jvm also has a similar mechanism. As long as we give a class with the same package name and class name as the android sdk, and write a method with the same method name, parameters, and return value, the jvm can compile and execute it.

(Hint: Android View and the like can also be done this way)

2. Isolate Native Methods

Xiaobai: "I use native methods, junit fails to run, and robolectric does not support loading so files. What should I do?"

Model class:

  1. package com.test.unit;
  2.  
  3. public class Model {
  4. public native boolean nativeMethod();
  5. }

Unit Tests:

  1. public class ModelTest {
  2.  
  3. Model model;
  4.  
  5. @Before
  6. public void setUp() throws Exception {
  7. model = new Model();
  8. }
  9.  
  10. @Test
  11. public void testNativeMethod() throws Exception {
  12. Assert.assertTrue(model.nativeMethod());
  13. }
  14. }

run ModelTest... error java.lang.UnsatisfiedLinkError: com.test.unit.Model.nativeMethod()

The "dependency isolation" mentioned in the previous article "Android Unit Testing - How to Start?" is used here!

Improved unit testing:

  1. public class ModelTest {
  2.  
  3. Model model;
  4.  
  5. @Before
  6. public void setUp() throws Exception {
  7. model = mock(Model.class);
  8. }
  9.  
  10. @Test
  11. public void testNativeMethod() throws Exception {
  12. when (model.nativeMethod()).thenReturn( true );
  13.  
  14. Assert.assertTrue(model.nativeMethod());
  15. }
  16. }

Run it again and it passes:

Here is a brief introduction to the process of finding native methods in Java:

1). The full name of Model.java is com.test.unit.Model.java;

2). After calling the native method nativeMethod(), the jvm will look for the C++ layer com_test_unit_Model.cpp, then find the com_test_unit_Model_nativeMethod() method and call it.

During the running process of the APP, we will compile the cpp file into a so file, and then let the APP load it into the dalvik virtual machine. But in the unit test, the corresponding so file is not loaded, and the cpp is not compiled! Experts may try to load the so file during unit testing, but it is completely unnecessary and does not conform to the principles of unit testing.

Therefore, we can directly use the Mockito framework to mock native methods. In fact, not only native methods need mocking, but also many dependent methods and classes need mocking. The following will discuss more common scenarios.

(Refer to "Android JNI Principle Analysis")

3. Solve the internal new object

Xiaobai: "I created a new Model in Presenter. Model has many dependencies and can perform SQL operations, etc. Presenter relies on Model to return results, which makes it impossible to unit test Presenter! Please help!"

Example of Xiaobai C: Model:

  1. public class Model {
  2. public boolean getBoolean() {
  3. boolean bo = ....... // A bunch of dependencies, the code is complicated
  4. return bo;
  5. }
  6. }

Presenter:

  1. public class Presenter {
  2.  
  3. Model model;
  4.  
  5. public Presenter() {
  6. model = new Model();
  7. }
  8.  
  9. public boolean getBoolean() {
  10. return model.getBoolean());
  11. }
  12. }

Incorrect unit test:

  1. public class PresenterTest {
  2.  
  3. Presenter presenter;
  4.  
  5. @Before
  6. public void setUp() throws Exception {
  7. presenter = new Presenter();
  8. }
  9.  
  10. @Test
  11. public void testGetBoolean() throws Exception {
  12. Assert.assertTrue(presenter.getBoolean());
  13. }
  14. }

Again, dependency isolation. We isolate Model dependencies, that is, mock Model objects instead of new Model().

Let's look at the problem with the PresenterTest above: PresenterTest is completely unaware of the existence of the Model, which means it cannot mock the Model. So, let's find a way to pass the mock Model to the Presenter - pass parameters in the Presenter constructor!

Improved Presenter:

  1. public class Presenter {
  2.  
  3. Model model;
  4.  
  5. public Presenter(Model model) {
  6. this.model = model;
  7. }
  8.  
  9. public boolean getBoolean() {
  10. return model.getBoolean();
  11. }
  12. }

Proper unit testing:

  1. public class PresenterTest {
  2. Model model;
  3. Presenter presenter;
  4.  
  5. @Before
  6. public void setUp() throws Exception {
  7. model = mock(Model.class); // mock Model object
  8.  
  9. presenter = new Presenter(model);
  10. }
  11.  
  12. @Test
  13. public void testGetBoolean() throws Exception {
  14. when (model.getBoolean()).thenReturn( true );
  15.  
  16. Assert.assertTrue(presenter.getBoolean());
  17. }
  18. }

The problem is solved. If you think it is more convenient to use the default Presenter constructor directly in the Activity and new Model() in the constructor, then keep the default constructor. Of course, when using dagger2, there are no multiple constructors, and all constructors pass parameters.

4. Static Methods

Xiaobai: "Master, I use static methods in Presenter..." Author: "Okay, I know what you mean."

Presenter:

  1. public class Presenter {
  2.  
  3. public String getSignParams( int uid, String name , String token) {
  4. return SignatureUtils.sign(uid, name , token);
  5. }
  6. }

The solution is similar to the above [Solving the problem of internal new objects], and the core idea is still to rely on isolation.

1). Change sign(...) to a non-static method;

2). Use SignatureUtils as a member variable;

3). The construction method passes in SignatureUtils;

4). When unit testing, pass mock SignatureUtils to Presenter.

Improved Presenter:

  1. public class Presenter {
  2. SignatureUtils mSignUtils;
  3.  
  4. public Presenter(SignatureUtils signatureUtils) {
  5. this.mSignUtils= signatureUtils;
  6. }
  7.  
  8. public String getSignParams( int uid, String name , String token) {
  9. return mSignUtils.sign(uid, name , token);
  10. }
  11. }

5. RxJava asynchronous to synchronous

Xiaobai: "Great God..."

Author: "I, the master, calculated with my fingers and predicted that you would encounter this disaster."

Xiaobai: (From the beginning to becoming a monk, as the legend goes?)

  1. public class RxPresenter {
  2.  
  3. public void testRxJava(String msg) {
  4. Observable.just(msg)
  5. .subscribeOn(Schedulers.io())
  6. .delay(1, TimeUnit.SECONDS) // Delay 1 second
  7. // .observeOn(AndroidSchedulers.mainThread())
  8. .subscribe(new Action1<String>() {
  9. @Override
  10. public void call(String msg) {
  11. System. out .println(msg);
  12. }
  13. });
  14. }
  15. }

Unit Testing

  1. public class RxPresenterTest {
  2.  
  3. RxPresenter rxPresenter;
  4.  
  5. @Before
  6. public void setUp() throws Exception {
  7. rxPresenter = new RxPresenter();
  8. }
  9.  
  10. @Test
  11. public void testTestRxJava() throws Exception {
  12. rxPresenter.testRxJava( "test" );
  13. }
  14. }

Run RxPresenterTest:

You will find that there is no output "test", why?

Because in testRxJava, Obserable.subscribeOn(Schedulers.io()) switches the thread to the io thread and delays for 1 second, while the testTestRxJava() unit test has already finished running in the current thread. I have tried it, and even if I remove delay(1, TimeUnit.SECONDS), it still does not output 'test'.

You can see that the author commented out .observeOn(AndroidSchedulers.mainThread()). If we add that code and run testTestRxJava() again, we will get java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.:

This is because jdk does not have the android.os.Looper class and related dependencies.

To solve the above two problems, we just need to switch Schedulers.io()&AndroidSchedulers.mainThread() to Schedulers.immediate(). The RxJava development team has thought about it for everyone and provided two hook operation classes, RxJavaHooks and RxAndroidPlugins.

Create RxTools:

  1. public class RxTools {
  2. public   static void asyncToSync() {
  3. Func1<Scheduler, Scheduler> schedulerFunc = new Func1<Scheduler, Scheduler>() {
  4. @Override
  5. public Scheduler call(Scheduler scheduler) {
  6. return Schedulers.immediate();
  7. }
  8. };
  9.  
  10. RxAndroidSchedulersHook rxAndroidSchedulersHook = new RxAndroidSchedulersHook() {
  11. @Override
  12. public Scheduler getMainThreadScheduler() {
  13. return Schedulers.immediate();
  14. }
  15. };
  16.  
  17. RxJavaHooks.reset();
  18. RxJavaHooks.setOnIOScheduler(schedulerFunc);
  19. RxJavaHooks.setOnComputationScheduler(schedulerFunc);
  20.  
  21. RxAndroidPlugins.getInstance().reset();
  22. RxAndroidPlugins.getInstance().registerSchedulersHook(rxAndroidSchedulersHook);
  23. }
  24. }

Add RxTools.asyncToSync(); to RxPresenterTest.setUp():

  1. public class RxPresenterTest {
  2. RxPresenter rxPresenter;
  3.  
  4. @Before
  5. public void setUp() throws Exception {
  6. rxPresenter = new RxPresenter();
  7.  
  8. RxTools.asyncToSync();
  9. }
  10. ...
  11. }

Run testTestRxJava() again:

Finally it outputs "test", thank God! (You should give me a reward^_^)

Did you find that RxTools.asyncToSync() has an additional line RxJavaHooks.setOnComputationScheduler(schedulerFunc), which means switching the computation thread to the immediate thread? I found that simply adding RxJavaHooks.setOnIOScheduler(schedulerFunc) still failed for the Obserable with delay, so I switched the computation thread as well, and it worked.

There are also RxJavaHooks.reset() and RxAndroidPlugins.getInstance().reset(). I found that when running a large number of unit tests, some would fail, but running the failed unit tests individually would pass again. After a lot of thinking, I added those two sentences... It works!

(Regarding the use of RxJavaHooks and RxAndroidPlugins, I have mentioned it in the article "(MVP+RxJava+Retrofit) Decoupling+Mockito Unit Testing Experience Sharing" a long time ago)

summary

Author: "Xiaobai, have you filled the hole you stepped on?"

Xiao Bai: "Abbot, no, Master, the above problems have been solved, but there are still some problems."

Author: "How can you fill the hole without digging it? I'll tell you about other unit testing secrets later."

noob:"......"

This article details the solutions to several important unit testing issues. It is not difficult for readers to find that the author has always emphasized dependency isolation, dependency isolation, and dependency isolation. This concept is very important in unit testing. Students who still don't understand this concept can read "Android Unit Testing - How to Get Started?" (another shameless advertisement) several times, and constantly review this concept in practice.

As long as these problems are solved, Presenter unit testing is not difficult. There are also SQLite and SharedPreferences unit tests that are not mentioned in this article, which will be introduced to readers in the following articles.

Thank you readers for your continued support. Please like and forward my article. May good people live in peace and happiness in their lives.

About the Author

I am a keyboard guy. I live in Guangzhou, work in a startup company, and am a sleazy, artistic programmer. I like science, history, investing, and occasionally traveling alone. I hope to become an independent engineer.

<<:  Three popular command line file conversion tools under Linux

>>:  A Preliminary Study on WeChat Mini Programs

Recommend

A strange cloud in the morning glow in Beijing, did you take a picture of it?

On the morning of December 16, a fiery glow appea...

How to correctly place YouTube TrueView video ads

Why run YouTube TrueView ads? YouTube TrueView di...

Breaking down the rules for taking notes on popular articles on Xiaohongshu!

This article analyzes the hottest articles on Xia...

Nana Options Academy First Session

Nana Options Academy's first phase resource i...

iOS scams collection

When I was making my first iOS app, I encountered...

VisionMobile: 2015 Mobile Developer Trends Report

The 2015 Mobile Developer Report surveyed more th...

Gamification design for user activation and retention

Gamification design can be seen in many products....

A "date" between ice and fire? A duet of Antarctic volcanoes

Cold, still, solidified, white - that is ice and ...

Android decompilation: decompilation tools and methods

[[126320]] Preface Sometimes during the developme...

What are those random things you see during a migraine?

Leviathan Press: When I was a kid, I would always...