构建有效的单元测试

发表于:2018-2-05 10:33

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

 作者:lanceJin    来源:简书

  1.前言
  单元测试是应用程序测试策略中的基本测试。通过对代码创建和运行单元测试,可以轻松验证独立逻辑单元是否正确。每次构建后运行单元测试,有助于快速捕获和修复代码改变引入的软件问题。通常以可重复的方式执行尽可能小的代码单元的功能(可能是方法、类或组件)。当需要验证应用程序中指定代码的逻辑,应该构建单元测试。例如,正在单元测试一个类,可能会检查类的状态是否正确。代码单元是独立测试的,只影响和监听指定单元的改变,模拟框架可被用来隔离单元和它的依赖。
  注意:单元测试不适合测试复杂的UI交互事件。应该使用UI测试框架,如自动化UI测试中描述的。
  为了测试安卓应用程序,通常会创建这些类型的自动化单元测试:
  ●本地测试:只运行于本地机器的单元测试。这些测试编译运行于Java虚拟机(JVM)来减少执行时间,不依赖安卓框架或者可以使用模拟对象替代依赖项。
  ●设备测试:运行在安卓设备或模拟器上的单元测试。这些测试可以访问仪器的信息,例如被测试应用程序的上下文,这些不易被模拟对象替代的安卓依赖项。
  下面将介绍如何构建这些类型的自动化单元测试。
  2.本地单元测试
  如果单元测试没有依赖或仅简单依赖安卓,应该在本地开发机器上运行测试。这种方式有助于避免每次运行测试,都加载目标应用程序和单元测试代码到物理设备或模拟器,大大减少单元测试的执行时间。为配合这种方式,通常使用类似Mockito的模拟框架来满足所有依赖关系。
  2.1.设置测试环境
  在Android Studio项目中,必须存储本地单元测试的源文件到模块名/src/test/java/目录(创建新项目时已存在)下。还需要使用JUnit 4框架提供的标准APIs,来配置项目的测试依赖。如果测试需要安卓的依赖配合,类似Mockito库可以简化本地单元测试,要了解关于使用模拟对象的更多信息,请参阅模拟Android依赖项。
  在应用程序顶层build.gradle文件中(即工程目录,若仅哪个模块需要,在该模块下配置),需要指定这些库作为依赖:
  dependencies {
      // Required -- JUnit 4 framework
      testCompile 'junit:junit:4.12'
      // Optional -- Mockito framework
      testCompile 'org.mockito:mockito-core:1.10.19'
  }
  2.2.创建本地单元测试类
  本地单元测试类应该写成JUnit 4测试类。JUnit是最流行和广泛使用的Java单元测试框架,它最新的版本相比之前,允许以更简洁和灵活的方式编写测试。不同于基于JUnit 3的Android单元测试的做法,JUnit 4不需要扩展junit.framework.TestCase类,也不需要为测试方法名称加test关键字作为前缀,同时不需要使用junit.framework或junit.extensions包中的任何类。
  创建基本的JUnit 4测试类(包含一个或多个测试方法的Java类)。每个测试方法以@Test注解开始,且包含用于执行和验证想要测试的组件中单一功能的代码。下面的例子展示如何实现本地单元测试类,测试方法emailValidator_CorrectEmailSimple_ReturnsTrue验证被测试的应用程序中isValidEmail()方法返回结果的正确性。
  import org.junit.Test;
  import java.util.regex.Pattern;
  import static org.junit.Assert.assertFalse;
  import static org.junit.Assert.assertTrue;
  public class EmailValidatorTest {
      @Test
      public void emailValidator_CorrectEmailSimple_ReturnsTrue() {
          assertThat(EmailValidator.isValidEmail("name@email.com"), is(true));
      }
      ...
  }
  为测试应用程序中的组件是否返回期望的结果,使用junit.Assert方法执行验证检查(或断言),来比较被测试组件的状态与一些期望的值。为了让测试更具可读性,可以使用Hamcrest匹配器(例如is()和equalTo()方法)来比较返回的结果和预期的结果。
  2.3.模拟安卓依赖项
  默认情况下,Gradle的安卓插件基于修改过的android.jar库(不包含任何实际代码),执行本地单元测试。在测试方法中调用安卓类时会引发异常,来确保只测试编写的代码,不依赖于Android平台的任何特有行为(当没有显式地模拟时)。
  可以使用模拟框架模拟代码中的外部依赖项,从而很容易地按期望的方式测试需要与依赖交互的组件。不仅将单元测试与Android系统的其余部分隔离,同时验证那些依赖项中的正确方法是否被调用。支持Java的Mockito模拟框架(1.9.5版本及以上)提供对安卓单元测试的兼容,可以配置模拟对象被调用时返回一些特定的值。若要使用此框架向本地单元测试添加模拟对象,请遵循以下开发步骤:
  ●按照设置测试环境那节中描述的,在build.gradle文件中添加对Mockito库的依赖。
  ●在单元测试类的定义之前,加上@RunWith(MockitoJUnitRunner.class)注解。这个注解告诉Mockito测试运行器去验证框架的使用是否正确,并且简化模拟对象的初始化。
  ●为安卓依赖项创建模拟对象时,在成员变量声明前添加@Mock注解。
  ●重写依赖项的行为,可以通过使用when()和thenReturn()方法,指定一个条件和当条件满足时返回的值。
  下面的例子展示如何使用模拟的上下文对象创建单元测试。
  import static org.hamcrest.MatcherAssert.assertThat;
  import static org.hamcrest.CoreMatchers.*;
  import static org.mockito.Mockito.*;
  import org.junit.Test;
  import org.junit.runner.RunWith;
  import org.mockito.Mock;
  import org.mockito.runners.MockitoJUnitRunner;
  import android.content.SharedPreferences;
  @RunWith(MockitoJUnitRunner.class)
  public class UnitTestSample {
      private static final String FAKE_STRING = "HELLO WORLD";
      @Mock
      Context mMockContext;
      @Test
      public void readStringFromContext_LocalizedString() {
          // Given a mocked Context injected into the object under test...
          when(mMockContext.getString(R.string.hello_word))
                  .thenReturn(FAKE_STRING);
          ClassUnderTest myObjectUnderTest = new ClassUnderTest(mMockContext);
          // ...when the string is returned from the object under test...
          String result = myObjectUnderTest.getHelloWorldString();
          // ...then the result should be the expected one.
          assertThat(result, is(FAKE_STRING));
      }
  }
  要了解更多关于使用Mockito框架,请参阅Mockito API参考和样例代码中的SharedPreferencesHelperTest类。
  2.4.Error:"Method ... not mocked"
  如果运行测试时,调用了安卓SDK中没有模拟的API,将会接收到一个此方法没有被模拟的错误,这是因为运行单元测试使用的android.jar文件不包含任何实际代码(这些APIs仅由设备上安卓系统镜像提供)。通过默认引发异常,来确保只测试编写的代码,不依赖于Android平台的任何特有行为(当没有显式地模拟时,例如使用Mockito)。当不希望测试中抛出异常时,可以通过给项目顶层的build.gradle文件(若仅针对模块,就用模块下的)中添加如下的配置来改变行为,让方法能够返回null或0:
  android {
    ...
    testOptions {
      unitTests.returnDefaultValues = true
    }
  }
  注意:设置returnDefaultValues属性为true时,应当谨慎。以null/0作为返回值会在测试中传递,这很难调试,而且可能会导致失败的测试通过,所以把它当作最后的手段。
  2.5.运行本地单元测试
  要运行本地单元测试,请执行以下步骤:
  ●通过点击工具栏中同步工程按钮,确保项目被Gradle同步。
  ●用下面的方式之一运行测试:
  运行单一测试方法,打开Project窗口,然后右击一个测试方法并点击Run选项。
  运行类中所有测试方法,右击这个类或测试文件中的方法并点击Run选项。
  运行目录下所有测试方法,右击这个目录并点击Run tests选项。
  Gradle的安卓插件会编译位于默认目录(src/test/java/)下的本地单元测试代码,构建一个测试应用程序,并且使用默认的测试运行器类来本地执行它,然后Android Studio在Run窗口中展示结果。
  3.设备单元测试
  设备单元测试是运行在物理设备和模拟器的测试,可以使用安卓框架APIs和支持的APIs,例如安卓测试支持库。当测试需要访问设备信息(例如目标应用程序的上下文)或需要安卓框架组件的真正实现(例如Parcelable或SharedPreferences对象)时,才创建设备单元测试。使用设备单元测试也有助于减少需要编写和维护模拟代码的工作量,同时可以使用模拟框架来模拟任何依赖关系。
  3.1.设置测试环境
  在Android Studio项目中,必须存储设备测试的源文件到模块名/src/androidTest/java/目录下,此目录创建新项目时已存在并包含设备测试样例。
  首先应该下载安卓测试支持库,它提供为应用程序快速构建和运行设备测试代码的APIs,同时包含JUnit 4测试运行器(AndroidJUnitRunner)和UI功能测试(Espresso和UI Automator)所需的APIs。接着,需要配置工程的安卓测试依赖项,来使用测试支持库提供的测试运行器和规定的APIs。为了简化测试开发,也应该包含Hamcrest库,从而使用它的匹配APIs来创建更灵活的断言。
  在应用程序顶层build.gradle文件中(即工程目录,若仅哪个模块需要,在该模块下配置),需要指定这些库作为依赖:
  dependencies {
      androidTestCompile 'com.android.support:support-annotations:24.0.0'
      androidTestCompile 'com.android.support.test:runner:0.5'
      androidTestCompile 'com.android.support.test:rules:0.5'
      // Optional -- Hamcrest library
      androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
      // Optional -- UI testing with Espresso
      androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
      // Optional -- UI testing with UI Automator
      androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
  }
  注意:如果构建配置的依赖包括compile注解支持库和androidTestCompileEspresso核心库,那么依赖冲突可能会导致构建失败。要解决此问题,按下面的方式更新对Espresso核心库的依赖:
  androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
      exclude group: 'com.android.support', module: 'support-annotations'
  })
  为了使用JUnit 4测试类,通过在app模块下的build.gradle文件中添加以下设置,确保指定AndroidJUnitRunner作为工程的默认设备测试运行器:
  android {
      defaultConfig {
          testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
      }
  }
  3.2.创建设备单元测试类
  设备单元测试类应该写成JUnit 4测试类。要了解更多关于创建JUnit 4测试类和使用JUnit 4断言及注解,请参阅创建本地单元测试类。为创建设备的JUnit 4测试类,在定义此类之前添加@RunWith(AndroidJUnit4.class)注解,也需要指定安卓测试支持库提供的AndroidJUnitRunner类为默认测试运行器。下面例子展示如何编写设备单元测试,来验证LogHistory类是否正确实现了Parcelable接口:
  import android.os.Parcel;
  import android.support.test.runner.AndroidJUnit4;
  import android.util.Pair;
  import org.junit.Test;
  import org.junit.runner.RunWith;
  import java.util.List;
  import static org.hamcrest.Matchers.is;
  import static org.junit.Assert.assertThat;
  @RunWith(AndroidJUnit4.class)
  @SmallTest
  public class LogHistoryAndroidUnitTest {
      public static final String TEST_STRING = "This is a string";
      public static final long TEST_LONG = 12345678L;
      private LogHistory mLogHistory;
      @Before
      public void createLogHistory() {
          mLogHistory = new LogHistory();
      }
      @Test
      public void logHistory_ParcelableWriteRead() {
          // Set up the Parcelable object to send and receive.
          mLogHistory.addEntry(TEST_STRING, TEST_LONG);
          // Write the data.
          Parcel parcel = Parcel.obtain();
          mLogHistory.writeToParcel(parcel, mLogHistory.describeContents());
          // After you're done with writing, you need to reset the parcel for reading.
          parcel.setDataPosition(0);
          // Read the data.
          LogHistory createdFromParcel = LogHistory.CREATOR.createFromParcel(parcel);
          List<Pair<String, Long>> createdFromParcelData = createdFromParcel.getData();
          // Verify that the received data is correct.
          assertThat(createdFromParcelData.size(), is(1));
          assertThat(createdFromParcelData.get(0).first, is(TEST_STRING));
          assertThat(createdFromParcelData.get(0).second, is(TEST_LONG));
      }
  }
  3.3.创建测试套件
  为组织设备单元测试的执行,可以收集一系列测试类到一个测试套件类中,然后一起运行这些测试。测试套件可以被嵌套,即收集其它测试套件到自己测试套件中,然后一起运行所有的测试类。测试套件包含在测试包中,类似于主应用程序包,命名通常以.suite结尾作为后缀(例如,com.example.android.testing.mysample.suite)。
  为单元测试创建测试套件,需导入JUnit中RunWith和Suite类。在套件中添加@RunWith(Suite.class)和@Suite.SuitClasses()注解,并在@Suite.SuitClasses()注解中分别列出测试类或测试套件作为参数。下面的例子展示,如何实现名为UnitTestSuite的测试套件,它收集并一起运行 CalculatorInstrumentationTest和CalculatorAddParameterizedTest测试类。
  import com.example.android.testing.mysample.CalculatorAddParameterizedTest;
  import com.example.android.testing.mysample.CalculatorInstrumentationTest;
  import org.junit.runner.RunWith;
  import org.junit.runners.Suite;
  // Runs all unit tests.
  @RunWith(Suite.class)
  @Suite.SuiteClasses({CalculatorInstrumentationTest.class,
          CalculatorAddParameterizedTest.class})
  public class UnitTestSuite {}
  3.4.运行设备单元测试
  ●按照下面这些步骤运行设备测试:
  ●通过点击工具栏中同步工程按钮,确保项目被Gradle同步。
  用下面的方式之一运行测试:
  运行单一测试方法,打开Project窗口,然后右击一个测试方法并点击Run选项。
  运行类中所有测试方法,右击这个类或测试文件中的方法并点击Run选项。
  运行目录下所有测试方法,右击这个目录并点击Run tests选项。
  Gradle的安卓插件会编译位于默认目录(src/androidTest/java/)下的设备测试代码,构建一个测试应用包和产品应用包,安装到连接的设备或模拟器上,并且运行测试,随后Android Studio在Run窗口中展示结果。
  注意:当运行或调试设备测试时,Android Studio不注入Instant Run所需的额外方法,并将功能关闭。
  3.5.在Firebase上运行测试
  使用Firebase测试实验室,可以在谷歌数据中心的物理和虚拟设备中,选择多款流行安卓设备和不同配置(地区、横竖屏、屏幕尺寸和平台版本),同时测试应用程序。可以从Android Studio或命令行,直接部署应用程序到测试实验室。测试结果提供日志,并包括应用程序失败的所有详细信息。
  在开始使用Firebase测试实验室之前,需要做到以下几点,除非已经拥有谷歌账号和Firebase工程:
  ●如果还没有,创建谷歌账号。
  ●在Firebase控制台中,点击Create New Project选项。
  在Spark计划的每日免费额度内,使用测试实验室测试应用程序不收取费用。Android Studio提供集成工具,用来配置希望如何部署测试到Firebase测试实验室。当按照规定的步骤创建完Firebase工程,就可以创建测试配置和运行测试:
  ●在主菜单点击Run > Edit Configurations选项。
  ●点击Add New Configuration选项并选择Android Tests。
  ●在安卓测试配置对话框内:
  输入或选择测试的详细信息,例如测试名称、模块类型、测试类型和测试类。
  从Deployment Target Options功能区的Target下拉菜单中,选择Firebase Test Lab Device Matrix选项。
  如果还没有登录,点击Connect to Google Cloud Platform,并允许Android Studio访问自己的账户。
  接着是Cloud Project,点击Settings按钮并从列表中选择自己的Firebase工程。
  ●创建和配置测试矩阵:
  接着是Matrix Configuration下拉列表,点击Open Dialog按钮。
  点击Add New Configuration (+)。
  在Name字段处,输入新配置的名字。
  选择想要测试应用程序的设备、安卓版本、区域和横竖屏。Firebase测试实验室将在选择的每种组合下测试应用程序,并生成测试结果。
  点击OK保存配置。
  ●点击Run/Debug Configurations对话框中的OK按钮退出。
  ●通过点击Run按钮运行测试。
  Config.png
  当Firebase测试实验室完整运行了测试,Run窗口将打开并显示结果,如下图所示。可能需要点击Show Passed按钮来查看所有执行过的测试。
  Results.png
  也可以通过点击Run窗口中,显示在测试执行日志开头的链接,到网页上分析测试。要了解更多关于网页展示结果的分析,请参阅分析Firebase安卓测试实验室的结果。
  3.6.附加示例代码
  要下载关于设备单元测试的示例应用程序,请参阅Android ActivityInstrumentation Sample。
  4.总结
  单元测试可以说是程序员在开发时,最常用的自检技术。通过它确定业务逻辑当中,输入和输出的对应关系(与函数式编程观点类似,有兴趣可以研究),方便开发人员确定自己编码的正确性,减少安装到设备上调试的频率,大大提高了工作效率,希望大家可以重视。


上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号