51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 773|回复: 0

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

[复制链接]
  • TA的每日心情
    无聊
    昨天 09:12
  • 签到天数: 918 天

    连续签到: 3 天

    [LV.10]测试总司令

    发表于 2022-7-28 09:37:37 | 显示全部楼层 |阅读模式
    前言
      单元测试是应用程序测试策略中的基本测试,通过对代码进行单元测试,可以轻松地验证单个单元的逻辑是否正确,在每次构建之后运行单元测试,可以帮助您快速捕获和修复因代码更改(重构、优化等)带来的回归问题。
      一、单元测试的目的以及测试内容
      1. 为什么要进行单元测试?
      · 提高稳定性,能够明确地了解是否正确的完成开发;
      · 快速反馈bug,跑一遍单元测试用例,定位bug;
      · 在开发周期中尽早通过单元测试检查bug,最小化技术债,越往后可能修复bug的代价会越大,严重的情况下会影响项目进度;
      · 为代码重构提供安全保障,在优化代码时不用担心回归问题,在重构后跑一遍测试用例,没通过说明重构可能是有问题的,更加易于维护。
      2. 单元测试要测什么?
      · 列出想要测试覆盖的正常、异常情况,进行测试验证;
      · 性能测试,例如某个算法的耗时等等。
      二、单元测试的分类
      1. 本地测试(Local tests): 只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如google推荐的Mockito;
      2. 仪器化测试(Instrumented tests): 在真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式。
      三、JUnit 注解
      · 定义所在方法为单元测试方法
    1. [url=home.php?mod=space&uid=724]@test[/url]
    2.   public void method()
    复制代码
    · 测试方法若没有抛出Annotation中的Exception类型(子类也可以)->失败

    1. @Test (expected = Exception.class)
    2.   public void method()
    复制代码
    性能测试,如果方法耗时超过100毫秒->失败

    1. @Test (expected = Exception.class)
    2.   public void method()
    复制代码
    这个方法在每个测试之前执行,用于准备测试环境(如: 初始化类,读输入流等),在一个测试类中,每个@Test方法的执行都会触发一次调用。

    1. [url=home.php?mod=space&uid=42644]@before[/url]
    2.   public void method()
    复制代码
    这个方法在每个测试之后执行,用于清理测试环境数据,在一个测试类中,每个@Test方法的执行都会触发一次调用。

    1. [url=home.php?mod=space&uid=16909]@after[/url]
    2.   public void method()
    复制代码
    这个方法在所有测试开始之前执行一次,用于做一些耗时的初始化工作(如: 连接数据库),方法必须是static。
    1. @BeforeClass
    2.   public static void method()
    复制代码
    这个方法在所有测试结束之后执行一次,用于清理数据(如: 断开数据连接),方法必须是static。
    1. @AfterClass
    2.   public static void method()
    复制代码
    忽略当前测试方法,一般用于测试方法还没有准备好,或者太耗时之类的。
    1. @Ignore或者@Ignore("太耗时")
    2.   public void method()
    复制代码
    使得该测试类中的所有测试方法都按照方法名的字母顺序执行,可以指定3个值,分别是DEFAULT、JVM、NAME_ASCENDING。
    1. @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    2.   public class TestClass{}
    复制代码
    四.本地测试
      根据单元有没有外部依赖(如Android依赖、其他单元的依赖),将本地测试分为两类,首先看看没有依赖的情况:
      · 添加依赖,google官方推荐
    1. dependencies {
    2.       // Required -- JUnit 4 framework
    3.       testImplementation 'junit:junit:4.12'
    4.       // Optional -- Mockito framework(可选,用于模拟一些依赖对象,以达到隔离依赖的效果)
    5.       testImplementation 'org.mockito:mockito-core:2.19.0'
    6.   }
    复制代码
    · 单元测试代码存储位置
      事实上,AS已经帮我们创建好了测试代码存储目录。
    1. app/src
    2.        ├── androidTestjava (仪器化单元测试、UI测试)
    3.        ├── main/java (业务代码)
    4.        └── test/java  (本地单元测试)
    复制代码
    · 创建测试类
      可以自己手动在相应目录创建测试类,AS也提供了一种快捷方式:选择对应的类->将光标停留在类名上->按下ALT + ENTER->在弹出的弹窗中选择Create Test。

    说明: 勾选 setUp/@Before??会生成一个带@Before注解的setUp()空方法,tearDown/@After则会生成一个带@After的空方法。
      · 运行测试用例
      运行单个测试方法:选中@Test注解或者方法名,右键选择Run;
      运行一个测试类中的所有测试方法:打开类文件,在类的范围内右键选择Run,或者直接选择类文件直接右键Run;
      运行一个目录下的所有测试类:选择这个目录,右键Run。
      · 运行前面测试验证邮箱格式的例子,测试结果会在Run窗口展示,如下图:

    从结果可以清晰的看出,测试的方法为 EmailValidatorTest 类中的 isValidEmail()方法,测试状态为passed,耗时12毫秒。
      修改一下前面的例子,传入一个非法的邮箱地址:
    @Test
      public void isValidEmail() {
          assertThat(EmailValidator.isValidEmail("#name@email.com"), is(true));
      }


    测试状态为failed,耗时14毫秒,同时也给出了详细的错误信息:在15行出现了断言错误,错误原因是期望值(Expected)为true,但实际(Actual)结果为false。
      也可以通过命令 gradlew test 来运行所有的测试用例,这种方式可以添加如下配置,输出单元测试过程中各类测试信息:
    android {
          ...
          testOptions.unitTests.all {
              testLogging {
                  events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
                  outputs.upToDateWhen { false }
                  showStandardStreams = true
              }
          }
      }

    还是验证邮箱地址格式的例子 gradlew test:

    在单元测试中通过System.out或者System.err打印的也会输出。
      · 通过模拟框架模拟依赖,隔离依赖
      前面验证邮件格式的例子,本地JVM虚拟机就能提供足够的运行环境,但如果要测试的单元依赖了Android框架,比如用到了Android中的Context类的一些方法,本地JVM将无法提供这样的环境,这时候模拟框架Mockito就派上用场了。
      · 一个Context#getString(int)的测试用例
    import static org.hamcrest.core.Is.is;
      import static org.junit.Assert.assertThat;
      import static org.mockito.Mockito.when;
      @RunWith(MockitoJUnitRunner.class)
      public class MockUnitTest {
          private static final String FAKE_STRING = "AndroidUnitTest";
          @Mock
          Context mMockContext;
          @Test
          public void readStringFromContext_LocalizedString() {
              //模拟方法调用的返回值,隔离对Android系统的依赖
              when(mMockContext.getString(R.string.app_name)).thenReturn(FAKE_STRING);
              assertThat(mMockContext.getString(R.string.app_name), is(FAKE_STRING));
              
              when(mMockContext.getPackageName()).thenReturn("com.jdqm.androidunittest");
              System.out.println(mMockContext.getPackageName());
          }
      }


    通过模拟框架Mockito,指定调用context.getString(int)方法的返回值,达到了隔离依赖的目的,其中Mockito使用的是cglib动态代理技术。








    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-3-28 20:40 , Processed in 0.065302 second(s), 25 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表