Android单元测试,你需要知道的一切(下)

发表于:2022-7-28 09:23

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:KevinDev0724    来源:51CTO博客

  五、仪器化测试
  在某些情况下,虽然可以通过模拟的手段来隔离Android依赖,但代价很大,这种情况下可以考虑仪器化的单元测试,有助于减少编写和维护模拟代码所需的工作量。
  仪器化测试是在真机或模拟器上运行的测试,它们可以利用Android framework APIs 和 supporting APIs。如果测试用例需要访问仪器(instrumentation)信息(如应用程序的Context),或者需要Android框架组件的真正实现(如Parcelable或SharedPreferences对象),那么应该创建仪器化单元测试,由于要跑到真机或模拟器上,所以会慢一些。
  · 配置
  dependencies {
      androidTestImplementation 'com.android.support:support-annotations:27.1.1'
      androidTestImplementation 'com.android.support.test:runner:1.0.2'
      androidTestImplementation 'com.android.support.test:rules:1.0.2'
  }
  ------------------------------分割线-----------------------------------
  android {
      ...
      defaultConfig {
          ...
          testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
      }
  }
  Example
  这里举一个操作SharedPreference的例子,这个例子需要访问Context类以及SharedPreference的具体实现,采用模拟隔离依赖的话代价会比较大,所以采用仪器化测试比较合适。
  这是业务代码中操作SharedPreference的实现:
  public class SharedPreferenceDao {
      private SharedPreferences sp;
      
      public SharedPreferenceDao(SharedPreferences sp) {
          this.sp = sp;
      }
      public SharedPreferenceDao(Context context) {
          this(context.getSharedPreferences("config", Context.MODE_PRIVATE));
      }
      public void put(String key, String value) {
          SharedPreferences.Editor editor = sp.edit();
          editor.putString(key, value);
          editor.apply();
      }
      public String get(String key) {
          return sp.getString(key, null);
      }
  }
  创建仪器化测试类(app/src/androidTest/java)
  // @RunWith 只在混合使用 JUnit3 和 JUnit4 需要,若只使用JUnit4,可省略
  @RunWith(AndroidJUnit4.class)
  public class SharedPreferenceDaoTest {
      public static final String TEST_KEY = "instrumentedTest";
      public static final String TEST_STRING = "玉刚说";
      SharedPreferenceDao spDao;
      @Before
      public void setUp() {
          spDao = new SharedPreferenceDao(App.getContext());
      }
      @Test
      public void sharedPreferenceDaoWriteRead() {
          spDao.put(TEST_KEY, TEST_STRING);
          Assert.assertEquals(TEST_STRING, spDao.get(TEST_KEY));
      }
  }
  运行方式和本地单元测试一样,这个过程会向连接的设备安装apk,测试结果将在Run窗口展示,如下图:
  通过测试结果可以清晰看到状态passed,仔细看打印的log,可以发现,这个过程向模拟器安装了两个apk文件,分别是app-debug.apk和app-debug-androidTest.apk,instrumented测试相关的逻辑在app-debug-androidTest.apk中。简单介绍一下安装apk命令pm install:
  // 安装apk
  //-t:允许安装测试 APK
  //-r:重新安装现有应用,保留其数据,类似于替换安装
  //更多请参考 https://developer.android.com/studio/command-line/adb?hl=zh-cn
  adb shell pm install -t -r filePath
  安装完这两个apk后,通过am instrument命令运行instrumented测试用例,该命令的一般格式:
  am instrument [flags] <test_package>/<runner_class>
  例如本例子中的实际执行命令:
  adb shell am instrument -w -r -e debug false -e class 'com.jdqm.androidunittest.SharedPreferenceDaoTest#sharedPreferenceDaoWriteRead' com.jdqm.androidunittest.test/android.support.test.runner.AndroidJUnitRunner
  -w: 强制 am instrument 命令等待仪器化测试结束才结束自己(wait),保证命令行窗口在测试期间不关闭,
  方便查看测试过程的log
  -r: 以原始格式输出结果(raw format)
  -e: 以键值对的形式提供测试选项,例如 -e debug false
  关于这个命令的更多信息请参考
  https://developer.android.com/studio/test/command-line?hl=zh-cn
  如果你实在没法忍受instrumented test的耗时问题,业界也提供了一个现成的方案Robolectric,下一小节讲开源框库的时候会将这个例子改成本地本地测试。
  六、常用单元测试开源库
  1. Mocktio
  https://github.com/mockito/mockito
  Mock 对象,模拟控制其方法返回值,监控其方法的调用等。
  · 添加依赖
  testImplementation 'org.mockito:mockito-core:2.19.0'
  Example
  import static org.hamcrest.core.Is.is;
  import static org.junit.Assert.*;
  import static org.mockito.ArgumentMatchers.anyInt;
  import static org.mockito.Mockito.*;
  import static org.mockito.internal.verification.VerificationModeFactory.atLeast;
  @RunWith(MockitoJUnitRunner.class)
  public class MyClassTest {
      @Mock
      MyClass test;
      @Test
      public void mockitoTestExample() throws Exception {
          //可是使用注解@Mock替代
          //MyClass test = mock(MyClass.class);
          // 当调用test.getUniqueId()的时候返回43
          when(test.getUniqueId()).thenReturn(18);
          // 当调用test.compareTo()传入任意的Int值都返回43
          when(test.compareTo(anyInt())).thenReturn(18);
          // 当调用test.close()的时候,抛NullPointerException异常
          doThrow(new NullPointerException()).when(test).close();
          // 当调用test.execute()的时候,什么都不做
          doNothing().when(test).execute();
          assertThat(test.getUniqueId(), is(18));
          // 验证是否调用了1次test.getUniqueId()
          verify(test, times(1)).getUniqueId();
          // 验证是否没有调用过test.getUniqueId()
          verify(test, never()).getUniqueId();
          // 验证是否至少调用过2次test.getUniqueId()
          verify(test, atLeast(2)).getUniqueId();
          // 验证是否最多调用过3次test.getUniqueId()
          verify(test, atMost(3)).getUniqueId();
          // 验证是否这样调用过:test.query("test string")
          verify(test).query("test string");
          // 通过Mockito.spy() 封装List对象并返回将其mock的spy对象
          List list = new LinkedList();
          List spy = spy(list);
          //指定spy.get(0)返回"Jdqm"
          doReturn("Jdqm").when(spy).get(0);
          assertEquals("Jdqm", spy.get(0));
      }
  }
  2. powermock
  https://github.com/powermock/powermock
  对于静态方法的 mock
  · 添加依赖
  testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
      testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
  Note: 如果使用了Mockito,需要这两者使用兼容的版本,具体参考
  https://github.com/powermock/powermock/wiki/Mockito#supported-versions
  Example
  @RunWith(PowerMockRunner.class)
  @PrepareForTest({StaticClass1.class, StaticClass2.class})
  public class StaticMockTest {
      @Test
      public void testSomething() throws Exception{
          // mock完静态类以后,默认所有的方法都不做任何事情
          mockStatic(StaticClass1.class);
          when(StaticClass1.getStaticMethod()).thenReturn("Jdqm");
           StaticClass1.getStaticMethod();
          //验证是否StaticClass1.getStaticMethod()这个方法被调用了一次
          verifyStatic(StaticClass1.class, times(1));
      }
  }
  或者是封装为非静态,然后用 Mockito :
  class StaticClass1Wraper{
    void someMethod() {
      StaticClass1.someStaticMethod();
    }
  3. Robolectric
  http://robolectric.org
  主要是解决仪器化测试中耗时的缺陷,仪器化测试需要安装以及跑在Android系统上,也就是需要在Android虚拟机或真机上面,所以十分的耗时,基本上每次来来回回都需要几分钟时间。针对这类问题,业界其实已经有了一个现成的解决方案: Pivotal实验室推出的Robolectric,通过使用Robolectrict模拟Android系统核心库的Shadow Classes的方式,我们可以像写本地测试一样写这类测试,并且直接运行在工作环境的JVM上,十分方便。
  · 添加配置
  testImplementation "org.robolectric:robolectric:3.8"
  android {
    ...
    testOptions {
      unitTests {
        includeAndroidResources = true
      }
    }
  }
  Example
  模拟打开MainActivity,点击界面上面的Button,读取TextView的文本信息。
  public class MainActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
          final TextView tvResult = findViewById(R.id.tvResult);
          Button button = findViewById(R.id.button);
          button.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                  tvResult.setText("Robolectric Rocks!");
              }
          });
      }
  }
  测试类(app/src/test/java/)
  @RunWith(RobolectricTestRunner.class)
  public class MyActivityTest {
      
      @Test
      public void clickingButton_shouldChangeResultsViewText() throws Exception {
          MainActivity activity = Robolectric.setupActivity(MainActivity.class);
          Button button =  activity.findViewById(R.id.button);
          TextView results = activity.findViewById(R.id.tvResult);
          //模拟点击按钮,调用OnClickListener#onClick
          button.performClick();
          Assert.assertEquals("Robolectric Rocks!", results.getText().toString());
      }
  }
  测试结果:
  耗时917毫秒,是要比单纯的本地测试慢一些。这个例子非常类似于直接跑到真机或模拟器上,然而它只需要跑在本地 JVM 即可,这都是得益于 Robolectric 的 Shadow 。
  七、实践
  1. 代码中用到了 TextUtil.isEmpty() 的如何测试
  public static boolean isValidEmail(CharSequence email) {
      if (TextUtils.isEmpty(email)) {
          return false;
      }
      return EMAIL_PATTERN.matcher(email).matches();
  }
  当你尝试本地测试这样的代码,就会收到一下的异常:
  java.lang.RuntimeException: Method isEmpty in android.text.TextUtils not mocked.
  这种情况,直接在本地测试目录(app/src/test/java)下添加TextUtils类的实现,但必须保证包名相同。
  package android.text;
  public class TextUtils {
      public static boolean isEmpty(CharSequence str) {
          return str == null || str.length() == 0;
      }
  }
  2. 隔离 native 方法
  public class Model {
      public native boolean nativeMethod();
  }
  public class ModelTest {
      Model model;
      @Before
      public void setUp() throws Exception {
          model = mock(Model.class);
      }
      @Test
      public void testNativeMethod() throws Exception {
          when(model.nativeMethod()).thenReturn(true);
          Assert.assertTrue(model.nativeMethod());
      }
  }
  3. 在内部 new,不方便 Mock
  public class Presenter {
      Model model;
      public Presenter() {
          model = new Model();
      }
      public boolean getBoolean() {
          return model.getBoolean());
      }
  }
  这种情况,需要改进一下代码的写法,不在内部 new,而是通过参数传递。
  public class Presenter {
      Model model;
      public Presenter(Model model) {
          this.model = model;
      }
      public boolean getBoolean() {
          return model.getBoolean();
      }
  }
  这样做方便Mock Model对象。
  public class PresenterTest {
      Model     model;
      Presenter presenter;
      
      @Before
      public void setUp() throws Exception {
          // mock Model对象
          model = mock(Model.class);
          presenter = new Presenter(model);
      }
      @Test
      public void testGetBoolean() throws Exception {
          when(model.getBoolean()).thenReturn(true);
          Assert.assertTrue(presenter.getBoolean());
      }
  }
  4. 本地单元测试-文件操作
  在一些涉及到文件读写的App,通常都会在运行时调用???Environment.getExternalStorageDirectory()??得到机器的外存路径,通常的做法是跑到真机或者模拟器上进行调试,耗时比较长,可以通过模拟的方式,在本地JVM完成文件操作。
  //注意包名保持一致
  package android.os;
  public class Environment {
      public static File getExternalStorageDirectory() {
          return new File("本地文件系统目录");
      }
  }
  直接在本地单元测试进行调试,不再需要跑到真机,再把文件pull出来查看。
  public class FileDaoTest {
      public static final String TEST_STRING = "Hello Android Unit Test.";
      
      FileDao fileDao;
      @Before
      public void setUp() throws Exception {
          fileDao = new FileDao();
      }
      @Test
      public void testWrite() throws Exception {
          String name = "readme.md";
          fileDao.write(name, TEST_STRING);
          String content = fileDao.read(name);
          Assert.assertEquals(TEST_STRING, content);
      }
  }
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号