• 0
  • 0
分享
  • Android 单元测试——软件测试圈
  • 北极 2021-06-04 10:17:26 字数 9102 阅读 1399 收藏 0

单元测试

单元测试是应用程序测试策略中的基本测试,通过对代码进行单元测试,可以轻松地验证单个单元的逻辑是否正确,在每次构建之后运行单元测试,可以帮助您快速捕获和修复因代码更改(重构、优化等)带来的回归问题。

一、单元测试的目的以及测试内容

为什么要进行单元测试

提高稳定性,能够明确地了解是否正确的完成开发;

快速反馈bug,跑一遍单元测试用例,定位bug;

在开发周期中尽早通过单元测试检查bug,最小化技术债,越往后可能修复bug的代价会越大,严重的情况下会影响项目进度;

为代码重构提供安全保障,在优化代码时不用担心回归问题,在重构后跑一遍测试用例,没通过说明重构可能是有问题的,更加易于维护。

单元测试要测什么?

列出想要测试覆盖的正常、异常情况,进行测试验证;

性能测试,例如某个算法的耗时等等。

二、单元测试的分类

1.本地测试(Local tests): module-name/src/test/

只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如google推荐的Mockito;

  • 优点:运行速度快、效率高。

  • 缺点:一般情况下,测试代码不能有Android依赖。若有Android依赖且要使用本地测试的方式,可以使用测试框架如Mockito来实现。

2.仪器化测试(Instrumented tests): module-name/src/androidTest/

在真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般的,依赖不太方便通过模拟框架模拟时采用这种方式。

  • 优点:测试代码直接支持对Android的依赖。

  • 缺点:需要真机或模拟器配合,运行速度较本地测试稍慢。实际上,在这类测试过程中是编译了一个额外的Apk,并安装到手机或模拟器中运行的。

测试框架选择

  • 目前流行的Android测试框架较多,按照对Android依赖的强弱情况,可以分为:

  • 无依赖:JUnit

  • 弱依赖:Mockito、 AndroidJUnitRunner

  • 强依赖:Espresso

三、JUnit

  • 本地测试(Local tests):配置

dependencies {
    testImplementation 'junit:junit:4.12'
    // Optional -- Mockito framework(可选,用于模拟一些依赖对象,以达到隔离依赖的效果)
    testImplementation 'org.mockito:mockito-core:3.5.13'  
}
  • 仪器化测试(Instrumented tests):配置(AndroidX)

dependencies {
    androidTestImplementation 'com.android.support:support-annotations:28.0.0'
    androidTestImplementation 'androidx.test:runner:1.3.0'
    androidTestImplementation 'androidx.test:rules:1.3.0'
    }
android {
    ...
    defaultConfig {
        ...
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}
  • @Test

public void method()

定义所在方法为单元测试方法

  • @Test (expected = Exception.class)

public void method()

测试方法若没有抛出Annotation中的Exception类型(子类也可以)->失败

  • @Test(timeout=100)

public void method()

性能测试,如果方法耗时超过100毫秒->失败

  • @Before

public void method()

这个方法在每个测试之前执行,用于准备测试环境(如: 初始化类,读输入流等),在一个测试类中,每个@Test方法的执行都会触发一次调用。

  • **@After **

public void method()

这个方法在每个测试之后执行,用于清理测试环境数据,在一个测试类中,每个@Test方法的执行都会触发一次调用。

  • @BeforeClass

public static void method()

这个方法在所有测试开始之前执行一次,用于做一些耗时的初始化工作(如: 连接数据库),方法必须是static

  • @AfterClass

public static void method()

这个方法在所有测试结束之后执行一次,用于清理数据(如: 断开数据连接),方法必须是static

  • @Ignore或者@Ignore(“太耗时”)

public void method()

忽略当前测试方法,一般用于测试方法还没有准备好,或者太耗时之类的

  • @FixMethodOrder(MethodSorters.NAME_ASCENDING)

public class TestClass{}

使得该测试类中的所有测试方法都按照方法名的字母顺序执行,可以指定3个值,分别是DEFAULT、JVM、NAME_ASCENDING

  • 单元测试代码存储位置

事实上,AS已经帮我们创建好了测试代码存储目录

app/src
     ├── androidTestjava (仪器化单元测试、UI测试)
     ├── main/java (业务代码)
     └── test/java  (本地单元测试)
  • 创建测试类

可以自己手动在相应目录创建测试类,AS也提供了一种快捷方式:选择对应的类->将光标停留在类名上->按下ALT + ENTER->在弹出的弹窗中选择Create Test

1.jpg

2.jpg

  • 运行测试用例

运行单个测试方法:选中@Test注解或者方法名,右键选择Run;

运行一个测试类中的所有测试方法:打开类文件,在类的范围内右键选择Run,或者直接选择类文件直接右键Run;

运行一个目录下的所有测试类:选择这个目录,右键Run。

(1)本地测试(Local tests):

业务类

public class WeekTestActivity extends Activity {
  public int findKthLargest(int[] nums, int k) {
        for (int i = 0; i < nums.length - 1; i++) {
            for (int j = 0; j < nums.length - 1; j++) {
                if (nums[j] < nums[j + 1]) {
                    int temp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = temp;
                }
            }
        }
        return nums[k - 1];
      }
    }

测试类

public class WeekTestActivityTest {
    @Test
    public void findKthLargestTest() {
        WeekTestActivity weekTestActivity = new WeekTestActivity();
        int arr[] = {26, 15, 29, 66, 99, 88, 36, 77, 111, 1, 6, 8, 8};
        int k = 4;
        int result = weekTestActivity.findKthLargest(arr, k);
        assertEquals(77, result);
    }
}

运行结果

3.png

这里返回code 0 是测试通过

修改预测结果

assertEquals(66, result);

报结果错误

在单元测试中通过System.out或者System.err打印的也会输出。

(2)仪器化测试

在某些情况下,虽然可以通过模拟的手段来隔离Android依赖,但代价很大,这种情况下可以考虑仪器化的单元测试,有助于减少编写和维护模拟代码所需的工作量。

仪器化测试是在真机或模拟器上运行的测试,它们可以利用Android framework APIs 和 supporting APIs。如果测试用例需要访问仪器(instrumentation)信息(如应用程序的Context),或者需要Android框架组件的真正实现(如Parcelable或SharedPreferences对象),那么应该创建仪器化单元测试,由于要跑到真机或模拟器上,所以会慢一些。

业务类

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);
    }
}

测试类

// @RunWith 只在混合使用 JUnit3 和 JUnit4 需要,若只使用JUnit4,可省略
@RunWith(AndroidJUnit4.class)
public class SharedPreferenceDaoTest {
    public static final String TEST_KEY = "express";
    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));
    }
}

运行结果

Testing started at 18:01 ...
09/29 18:01:46: Launching 'sharedPreferenceDa...()' on Xiaomi Redmi 7.
Running tests
$ adb shell am instrument -w -r    -e debug false -e class 'com.app.wangxu.testapplication.SharedPreferenceDaoTest#sharedPreferenceDaoWriteRead' com.app.wangxu.testapplication.test/androidx.test.runner.AndroidJUnitRunner
Connected to process 16726 on device 'xiaomi-redmi_7-693cd27a'.
Started running tests
Tests ran to completion.

四、常用单元测试开源库Mock的概念:两种误解

Mock的概念,其实很简单,我们前面也介绍过:所谓的mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:

验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等

指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作

JUnit 是单元测试框架。Mockito 与 JUnit 不同,并不是单元测试框架(这方面 JUnit 已经足够好了),它是用于生成模拟对象或者直接点说,就是”假对象“的工具。两者定位不同,所以一般通常的做法就是联合 JUnit + Mockito 来进行测试。

使用时在build文件中添加依赖

dependencies {
    // Optional -- Mockito framework(可选,用于模拟一些依赖对象,以达到隔离依赖的效果)
    testImplementation 'org.mockito:mockito-core:3.5.13'  
}

1.四种Mock方式

  • 普通方法:

public class MockitoTest {
    @Test
    public void testIsNotNull(){
        Person mPerson = mock(Person.class); //<--使用mock方法
        assertNotNull(mPerson);
    }
}
  • 注解方法:

public class MockitoAnnotationsTest {
    @Mock //<--使用@Mock注解
    Person mPerson;
    @Before
    public void setup(){
        MockitoAnnotations.initMocks(this); //<--初始化
    }
    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }
}
  • 运行器方法:

@RunWith(MockitoJUnitRunner.class) //<--使用MockitoJUnitRunner
public class MockitoJUnitRunnerTest {
    @Mock //<--使用@Mock注解
    Person mPerson;
    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }
}
  • MockitoRule方法

public class MockitoRuleTest {
    @Mock //<--使用@Mock注解
    Person mPerson;
    
    @Rule //<--使用@Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }
}

其中后两种方法是结合JUnit框架去实现的

2.常用打桩方法

因为Mock出的对象中非void方法都将返回默认值,比如int方法将返回0,对象方法将返回null等,而void方法将什么都不做。“打桩”顾名思义就是将我们Mock出的对象进行操作,比如提供模拟的返回值等,给Mock打基础。

方法名方法描述
thenReturn(T value) 设置要返回的值
thenThrow(Throwable… throwables) 设置要抛出的异常
thenAnswer(Answer<?> answer对结果进行拦截
doReturn(Object toBeReturned)提前设置要返回的值
doThrow(Throwable… toBeThrown)提前设置要抛出的异常
doAnswer(Answer answer) 提前对结果进行拦截
doCallRealMethod() 调用某一个方法的真实实现
doNothing() 设置void方法什么也不做

3.常用验证方法

前面所说的都是状态测试,但是如果不关心返回结果,而是关心方法有否被正确的参数调用过,这时候就应该使用验证方法了。
从概念上讲,就是和状态测试所不同的“行为测试”了。

verify(T mock)验证发生的某些行为

方法名方法描述
after(long millis) 在给定的时间后进行验证
timeout(long millis) 验证方法执行是否超时
atLeast(int minNumberOfInvocations) 至少进行n次验证
atMost(int maxNumberOfInvocations)至少进行n次验证
description(String description) 验证失败时输出的内容
times(int wantedNumberOfInvocations) 验证调用方法的次数
never()验证交互没有发生,相当于times(0)
only() 验证方法只被调用一次,相当于times(1)
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {
    @Mock
    private ArrayList mockList;
    @Mock
    private Person mPerson;
    @Test
    public void testIsNotNull() {
        Assert.assertNotNull(mockList);
    }
    @Test
    public void sampleTest() throws Exception {
     Mockito.doAnswer(invocation -> {
            System.out.println("测试无返回值的函数");
            return null;
        }).when(mockList).clear();
        
        mockList.add("sampleTest");
        mockList.clear();
        Mockito.verify(mockList).add("sampleTest");
        Mockito.verify(mockList).clear();
    }
    @Test
    public void testPersonAnswer() {
        Mockito.when(mPerson.getName()).thenReturn("小明");
        Mockito.when(mPerson.getKey()).thenReturn("www.baidu.com");
        System.out.print(mPerson.getName());
        System.out.print(mPerson.getKey());
    }
}

4.其他方法


方法名 方法描述
reset(T … mocks) 重置Mock
spy(Class classToSpy) 实现调用真实对象的实现
inOrder(Object… mocks) 验证执行顺序
@InjectMocks注解自动将模拟对象注入到被测试对象中
@Spy
public class MockitoSpyTest {
    @Spy
    Person mPerson;
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }
    @Test
    public void testPersonSpy(){
        //输出11
        System.out.print(mPerson.getAge());
    }
}

2.inOrder使用代码及测试结果:

5.jpg

3.@InjectMocks: 创建一个实例,这个实例需要的参数用@Mock(或@Spy)注解创建的注入到该实例中。

public class Home {
    private Person mPerson;
    public Home(Person person) {
        mPerson = person;
    }
    public String getMaster(){
        return mPerson.getName();
    }
}

6.jpg

其他:Mockito框架不支持mock匿名类、final类、static方法、private方法


作者:Sunny_Snail

原文链接:https://blog.csdn.net/qq_32324617/article/details/108826874

  • 【留下美好印记】
    赞赏支持
登录 后发表评论
+ 关注

热门文章

    最新讲堂

      • 推荐阅读
      • 换一换
          • 读者提问:WEB 自动化测试工具有推荐的吗 ?阿常回答:有,Selenium。官网地址:https://www.selenium.devGithub地址:https://github.com/selenium/selenium (开源社区)阿常碎碎念:Selenium 测试直接在浏览器中运行,就像真实用户所做的一样。Selenium 测试可以在 Windows、Linux 和 Macintosh上的 Internet Explorer、Chrome 和 Firefox中运行。Selenium完全开源,对商业用户也没有任何限制,支持分布式,拥有成熟的社区与学习文档。想更深入的了解,可去...
            0 0 708
            分享
          • PerfMon并不是JMeter原生的工具。要使用这个工具还需要下载一些插件。1、JMeter Plugins Manager下载:该插件是一个管理插件的插件。下载页面(https://jmeter-plugins.org/downloads/all/),下载后是一个jar(jmeter-plugins-manager-0.10.jar),把该jar放到jmeter的lib/ext目录下,重启jmeter即可。jar下载地址:https://jmeter-plugins.org/get/说明地址:https://jmeter-plugins.org/wiki/PluginsManager/重启...
            12 12 1044
            分享
          • 三种无效的BugBy Design:设计需求就是这么设计的,无效的BugDuplicate:这个问题别人已经发现,重复的BugNot Repro:无法复现的问题,无效的Bug四种有效的BugFixed:问题被修复External:外部原因(比如浏览器、操作系统、其他第三方软件)造成的问题Postponed:是个问题,发现的太晚了,目前不必修理了,下一个版本讨论是否解决或推迟到以后再解决Won’t?Fix:是个问题,但是不值得修复?,不管它?三维bug定义:bug三维:严重程度,处理优先级,影响范围。Bug严重程度。由Bug的创建者视情况来指定,其中1为最严重的问题,4为最小的问题。一般来讲,1...
            12 12 979
            分享
          •  绩效面谈结束,从会议室出来,有一种不真实的感觉——这个季度我竟然拿了S?!要知道,上个季度和上上个季度,我的绩效是C。 C在我司意味着什么你们知道吗? 拿3次C就意味着严重不胜任,公司就会让我收拾收拾包袱滚蛋。而我,那时候,离第三个C,就只差一个季度了。回过头来看,还是很感慨的,我真的在三个月的时间里,从部门里的渣渣绝地逆袭了。但其实只有我知道,我不是靠自己做到的,而是有人不离不弃地拉了我一把。01我在这家公司干后端开发干到第二年,表现开始下滑,拿到第二个C的时候,我真的心灰意懒, leader 跟我说结果的时候,我心里都盘算着开始写简历了。但是我 leader 却很严肃地问我究竟...
            0 0 674
            分享
          • 浏览器兼容性测试这是兼容性测试的子类型(如下所述),由测试团队执行。浏览器兼容性测试 针对 Web 应用程序执行,并确保软件可以在不同浏览器和操作系统的组合下运行。这种类型的测试还验证 Web 应用程序是否在所有浏览器的所有版本上运行。向后兼容性测试这是一种测试,用于验证新开发的软件或更新的软件是否适用于旧版本的环境。向后兼容性测试检查新版本的软件是否与旧版本软件创建的文件格式正常工作。它还适用于由该软件的旧版本创建的数据表、数据文件和数据结构。如果更新了任何软件,那么它应该可以在该软件的先前版本之上运行良好。黑盒测试此类测试不考虑内部系统设计。测试基于需求和功能。可以在此处找到有关...
            0 0 1221
            分享
      • 51testing软件测试圈微信