自动化测试集成指南 -- 本地单元测试

这篇具有很好参考价值的文章主要介绍了自动化测试集成指南 -- 本地单元测试。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

构建本地单元测试

简介:

单元测试(Unit Test) 是针对 程序的最小单元 来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。一个单元可能是单个程序、类、对象、方法等。

如何区分单元测试和集成测试,一般情况下,单元测试应该不依赖数据库,网络,I/O和其他模块,否则就是集成测试

单元测试特性

  • 单元性(快速)
    • 测试力度足够小,能够精确定位问题
  • 单一职责:一个测试case只负责一条路径,测试代码中不允许有复杂的逻辑条件
  • 独立性(无依赖)
    • 避免单元测试之间的依赖关系,一个测试的运行不依赖于其他测试代码的运行结果
  • 不依赖数据:与数据库交互时不能假设数据存在,可调用Dao中的Insert方法来准备数据
  • 不依赖外部环境,建议使用Mock
  • 可重复性(幂等性)
    • 不破坏数据:和数据库相关的单元测试,必须设定自动回滚机制@Transactional
  • 每次执行的结果都相同
  • 自验证
    • 不靠人来检查,必须使用断言
  • 尽可能断言具体的内容(简单的为空判断起不到太大的作用)
  • 测试代码必须有好的前置条件和后置断言
  • 异常的验证使用expectedExceptions

单元测试规范

基本准则:

  1. 【强制】核心应用核心业务增量代码一定要写单元测试。
  2. 【强制】单元测试类的命名应该和被测试类保持一致xxxTest,测试方法为被测方法名testXxx,所在包的命名和被测试类的包名保持一致。注意:单元测试持续集成的时候会用到maven插件maven-surefire-plugin,这个插件目标会自动执行测试源码路径(默认为src/test/Java/)下所有符合命名模式的测试类。模式为:*/Test.java:任何子目录下所有命名以Test结尾的Java类。
  3. 【强制】单元测试应该是全自动/非交互式的,测试套件通常是定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证、代码执行路径方式验证(mock)或者数据存在性验证(dao数据库操作)。
  4. 【强制】保持测试的独立性,为了保证测试稳定可靠且便于维护,测试用例之间决不能有相互依赖,也不能依赖执行的先后次序。
  5. 【强制】开发必须保证自己写的单元测试能在本地执行通过,才能提交。
  6. 【强制】重视边界值测试。
  7. 【强制】不需要的单元测试直接删除,不要注释掉,如果非要注释请写清楚注释理由。
  8. 【推荐】对于单元测试,要保证测试力度足够小,能够精确定位问题。对于依赖本应用之外的所有第三方环境的单元测试,统一使用Mock的方式进行测试,即做到尽可能的摆脱对环境依赖、持续重复运行。
  9. 【推荐】写测试代码的目的是为了提高业务代码质量,严禁为达到测试要求而书写不规范测试代码;对于不可测的代码建议做必要的重构,使代码变的可测

目录&命名规范

目录:

  • src/test/java

文件命名规范:

  • 单元测试包结构和源码结构必须保持一致,如下图所示:
  • 自动化单元测试,单元测试,java
  • 单元测试文件名字是由“被测试文件名 + Test”组成,如下图所示:
  • 测试方法:

    • 1:1情形: testXxx
  • 1:N情形: testXxxx_测式场景

前言

如何判断应该使用什么框架来书写测试用例:

  1. 纯java函数可以使用Junit来进线书写单元测试和断言,如下图所示:

public class Utils {

public static boolean isNumeric(String str) {

for (int i = str.length(); --i >= 0;) {

if (!Character.isDigit(str.charAt(i))) {

return false;

}

}

return true;

}

}
public class UtilsTest {

@Test

public void testIsNumeric() {

String testData = "1233";

boolean isNumeric = Utils.isNumeric(testData);

Assert.assertTrue(isNumeric);

//false

String testErrorData = "1233aaa";

boolean isErrorNumeric = Utils.isNumeric(testErrorData);

Assert.assertFalse(isErrorNumeric);

}

}
  1. 有安卓相关方法需要使用其他单元测试框架,但是需要测试的函数需要根据实际情况来选择对应框架,我们比较倾向于两种安卓相关的单元测试框架PowerMock(是用来 Mock 依赖的类或者接口,对那些不容易构建的对象用一个虚拟对象来代替,实现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持),Robolectric(在 JVM 中实现了 Android SDK 运行的环境,让我们无需运行虚拟机/真机就可以跑单元测试) 。

什么时候使用PowerMock:

  1. 依赖其他模块的类或者接口mock出需要测试的对象来进行单测,具体可以参考下面示例(Dao mock示例),这个示例可以比较直观的看出mock了一个User对象来进行单元测试的。

什么时候使用Robolectric:

  1. 有上下文context 相关方法可以使用 (application = ApplicationProvider.getApplicationContext();)
  2. Android原生控件相关测试

在写测试用例时,我们会必然用到一种场景:一个测试类中的方法, 有的需要在Robolectric运行器中 ,有的需要在PowerMock 运行器中进行测试,针对这种场景我们需要做一下简单的配置即可,在RobolectricTestRunner 运行器中使用PowerMock的MockitoRule 方式进行使用。 如下示例所示:

以ImageUtils的rotate()方法为例(在本例中没必要使用Robolectric,单纯为了举例)

 
/**

* Return the rotated bitmap.

*

* @param src The source of bitmap.

* @param degrees The number of degrees.

* @param px The x coordinate of the pivot point.

* @param py The y coordinate of the pivot point.

* @param recycle True to recycle the source of bitmap, false otherwise.

* @return the rotated bitmap

*/

public static Bitmap rotate(final Bitmap src,

final int degrees,

final float px,

final float py,

final boolean recycle) {

if (isEmptyBitmap(src)) return null;

if (degrees == 0) return src;

Matrix matrix = new Matrix();

matrix.setRotate(degrees, px, py);

Bitmap ret = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true);

if (recycle && !src.isRecycled() && ret != src) src.recycle();

return ret;

}

测试方法如下,在此次测试中,RunWith设置的是RobolectricTestRunner,同样mock了Bitmap对象。

@RunWith(RobolectricTestRunner.class)

@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*", "org.powermock.*" })

@PrepareForTest(Bitmap.class)

@Config(sdk = 28)

public class imageUtilsTest2 {

@Rule

public PowerMockRule rule = new PowerMockRule();

private Bitmap bitmap;

@Before

public void init() {

MockitoAnnotations.initMocks(this);

PowerMockito.mockStatic(Bitmap.class);

bitmap = PowerMockito.mock(Bitmap.class);

}

@Test

public void imagetest() {

when(bitmap.getWidth()).thenReturn(100);

when(bitmap.getHeight()).thenReturn(100);

when(bitmap.createBitmap(any(Bitmap.class), anyInt(), anyInt(), anyInt(), anyInt(),

any(Matrix.class), anyBoolean())).thenReturn(bitmap);

assertNotNull(ImageUtils.rotate(bitmap, 0, 10, 10, false));

assertNull(ImageUtils.rotate(null, 0, 10, 10, false));

assertNotNull(ImageUtils.rotate(bitmap, 0, 10, 10, true));

assertNull(ImageUtils.rotate(null, 0, 10, 10, true));

}

参考:

研发工程师首先需要罗列出项目主干逻辑,这样有利于单元测试的开展。 第一步以非UI类代码入手,如:Utils类中的公共方法、和UI无关、比较独立的方法先开始接入,先挑一些比较重要的case做。

如何运行单元测试

你可以在 Android Studio 中或从命令行运行测试。

在 Android Studio 中项目 APP test 目录下

  1. AS ——Select Run/Debug —— Configuration ——Edit Configuration ——配置 ——OK
  2. 如果想测试单个测试用例的方法或者类,右键选中要测试的方法或类(比如:Run listGoesOverTheFold(),Run HelloWorldEspressoTest),直接Run 选中的方法或者类名即可
  3. 想要全部 Run 所有测试用例可以选择整个 test 下的 java 文件夹,右键 Run All Tests

JUnit

JUnit是Java单元测试的根基,基本上都是通过断言来验证函数返回值/对象的状态是否正确。测试用例的运行和验证都依赖于它来进行。

JUnit的用途主要是:

  1. 提供了若干注解,轻松地组织和运行测试。
  2. 提供了各种断言api,用于验证代码运行是否符合预期。
  3. 在Android项目中纯java函数可以使用junit

简单介绍一下几个常用注解:

@Test

表示此方法为测试方法

@Before

在每个测试方法前执行,可做初始化操作

@After

在每个测试方法后执行,可做释放资源操作

@Ignore

忽略的测试方法

@BeforeClass

在类中所有方法前运行。此注解修饰的方法必须是static void

@AfterClass

在类中最后运行。此注解修饰的方法必须是static void

@RunWith

指定该测试类使用某个运行器(Runner的概念)

@Parameters

指定测试类的测试数据集合

@Rule

重新制定测试类中方法的行为

@FixMethodOrder

指定测试类中方法的执行顺序

ps: 一个测试类单元测试的执行顺序为: @BeforeClass –> @Before –> @Test –> @After –> @AfterClass

常用的断言,也可以自行查阅官方wiki。

assertEquals

断言传入的预期值与实际值是相等的

assertNotEquals

断言传入的预期值与实际值是不相等的

assertArrayEquals

断言传入的预期数组与实际数组是相等的

assertNull

断言传入的对象是为空

assertNotNull

断言传入的对象是不为空

assertTrue

断言条件为真

assertFalse

断言条件为假

assertSame

断言两个对象引用同一个对象,相当于“==”

assertNotSame

断言两个对象引用不同的对象,相当于“!=”

assertThat

断言实际值是否满足指定的条件

构建测试环境

在 Android Studio 项目中,你必须将本地单元测试的源文件存储在 module-name/src/test/java/ 中。当你创建新项目时,此目录已存在。

在应用的顶级 build.gradle 文件中,请将以下库指定为依赖项 (若已存在则不需要添加):

 
dependencies {

// Required -- JUnit 4 framework

testImplementation 'junit:junit:4.12'

}

创建本地单元测试类

你的本地单元测试类应编写为 JUnit 4 测试类。JUnit 是最受欢迎且应用最广泛的 Java 单元测试框架。与原先的版本相比,JUnit 4 可让你以更简洁且更灵活的方式编写测试,因为 JUnit 4 不要求你执行以下操作:

  • 扩展 junit.framework.TestCase 类。
  • 在测试方法名称前面加上 'test' 关键字作为前缀。
  • 使用 junit.frameworkjunit.extensions 软件包中的类。

如需创建基本的 JUnit 4 测试类,请创建包含一个或多个测试方法的类。测试方法以 @Test 注释开头,并且包含用于运用和验证要测试的组件中的单项功能的代码。

以下示例展示了如何实现本地单元测试类:

验证是否返回正确的结果。

import com.google.common.truth.Truth.assertThat;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)

public class EmailValidatorTest {

@Test

public void emailValidator_CorrectEmailSimple_ReturnsTrue() {

//断言实际值是否满足指定的条件

assertThat(EmailValidator.isValidEmail("name@email.com")).isTrue();

}

}

对象的判空校验:

@RunWith(JUnit4.class)

public class JUnitSample {

Object object;

//初始化方法,通常进行用于测试的前置条件/依赖对象的初始化

@Before

public void setUp() throws Exception {

object = new Object();

}

//测试方法,必须是public void

@Test

public void test() {

Assert.assertNotNull(object);

}

//在每个测试方法后执行,可做释放资源操作

@After

public void close() {

}

}

计算器示例

 
public class Calculator {

//加

public int add(int a, int b) {

return a + b;

}

//减

public int subtract(int a, int b) {

return a - b;

}

//乘

public int multiply(int a, int b) {

return a * b;

}

//除

public int divide(int a, int b) throws Exception {

if (0 == b) {

throw new Exception("除数不能为0");

}

return a / b;

}

}

测试用例

 
public class CalculatorTest {

private Calculator mCalculator;

//初始化方法,通常进行用于测试的前置条件/依赖对象的初始化

@Before

public void setup() {

mCalculator = new Calculator();

}

@Test

public void testAdd() {

int result = mCalculator.add(1, 2);

//断言传入的预期值与实际值是相等的

Assert.assertEquals(3, result);

}

@Test

public void testSubtract() {

int result = mCalculator.subtract(1, 2);

//断言传入的预期值与实际值是相等的

Assert.assertEquals(-1, result);

}

@Test

public void testMultiply() {

int result = mCalculator.multiply(1, 2);

//断言传入的预期值与实际值是相等的

Assert.assertEquals(2, result);

}

@Test

public void testDivide() {

int result = 0;

try {

result = mCalculator.divide(4, 2);

} catch (Exception e) {

e.printStackTrace();

Assert.fail();

}

//断言传入的预期值与实际值是相等的

Assert.assertEquals(2, result);

}

//在每个测试方法后执行,可做释放资源操作

@After

public void close() {

}

}

JUnit4 验证是否抛出异常

  1. expected声明方式

@Test(expected= IllegalArgumentException.class)

public void shouldNotAddNegativeWeights() {

weightCalculator.addItem(-5);

}
  1. @Rule方式

@Rule

public ExpectedException thrown = ExpectedException.none();

@Test

public void shouldNotAddNegativeWeights() {

thrown.expect(IllegalArgumentException.class);

thrown.expectMessage("Cannot add negative weight");

weightCalculator.addItem(-5);

}

Powermock

在看PowerMockito 之前先说下什么是Mockito, 因为PowerMockito是基于Mockito的扩展。

Mockito简介

Mocktio是Mock的工具类,主要是Java的类库,Mock就是伪装的意思。他们适用于单元测试中,对于单元测试来说,我们不希望依赖于第三方的组件,比如数据库、Webservice等。在写单元测试的时候,我们如果遇到了这些需要依赖第三方的情况,我们可以使用mock的技术,伪造出来我们自己想要的结果。对于Java而言,mock的对象主要是Java 方法和 Java类。 但是Mocktio也有它的不足之处,因为Mocktio不能mock static、final、private等对象,这时候就引出了Powermock框架。

Powermock简介

PowerMock 也是一个单元测试模拟框架,它是在Mockito 单元测试模拟框架的基础上做出的扩展,所以二者的api都非常相似。 通过提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMock 实现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持,对静态初始化过程的移除等强大的功能。因为 PowerMock 在扩展功能时完全采用和被扩展的框架相同的 API, 熟悉 PowerMock 所支持的模拟框架的开发者会发现 PowerMock 非常容易上手。PowerMock 的目的就是在当前已经被大家所熟悉的接口上通过添加极少的方法和注释来实现额外的功能。当Powermock和mockito结合使用的时候,我们需要考虑兼容性的问题。两者的版本需要兼容,如下图所示:

PowerMock主要是对Mockito增强,主要是以下几个常用场景

  • mock静态方法
  • 跳过私有方法
  • 更改子类无法访问的父类私有field
  • 更改类的私有static常量
  • 模拟New构造函数

添加配置和依赖:

 
android {

testOptions {

unitTests {

includeAndroidResources = true

}

}

}


testImplementation 'junit:junit:4.12'

//三方单元测试框架

testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'

testImplementation 'org.powermock:powermock-module-junit4:2.0.0'

testImplementation 'org.powermock:powermock-module-junit4-rule:2.0.0'

testImplementation 'org.powermock:powermock-classloading-xstream:2.0.0'

使用 PowerMock,首先需要使用@RunWith(PowerMockRunner.class)将测试用例的 Runner 改为PowerMockRunner。如果要 Mockstaticfinalprivate等方法的时候,就需要加注解@PrepareForTest

在项目根目录下的gradle.properties文件中添加下面的配置(在Android Studio 3.3+以上不需要):

 

android.enableUnitTestBinaryResources=true

常用api:

Mock

Powermockito.mock() 方 法 主 要 是 根 据 class 创 建 一 个 对 应 的 mock 对 象 ,

powermock 的创建方式可不像 easymock 等使用 proxy 的方式创建,他是会在你运行的

过程中动态的修改 class 字节码文件的形式来创建的。

spy

如果一个对象,只希望mock它的部分方法,而其他方法希望和真实对象的行为一致,可以使用spy。时,没有通过when设置过的方法,测试调用时,行为和真实对象一样

DoReturn…when…then

我们可以看到,每次当我们想给一个 mock 的对象进行某种行为的预期时,都会使用

do…when…then…这样的语法,其实理解起来非常简单:做什么、在什么时候、然后返回

什么。DoReturn不会进入mock方法的内部

when…thenReturn

其实理解起来非常简单:做什么、在什么时候、然后返回

什么。需要注意的是:mock的对象,所有没有调用when设置过的方法,在测试时调用,返回的都是对应返回类型的默认值。when…thenReturn会进入mock方法的内部

doNothing().when(…)…

调用后什么都不做的

doThrow(Throwable).when(…)…

调用后抛异常

Verify

当我们测试一个 void 方法的时候,根本没有办法去验证一个 mock 对象所执行后的结

果,因此唯一的方法就是检查方法是否被调用,在后文中将还会专门来讲解。

@PrepareForTest

PowerMock的Runner提前准备一个已经根据某种预期改变过的class,PowerMockito mock私有方法,静态方法和final方法的时候添加这个注解,可以作用在类和方法(某些情况下不起作用)上

注意点:

如果一个测试类中有被@PrepareForTest(XXX.class)修饰的方法,如果测试类中有多个测试方法,单独 Run 被PrepareForTest 修饰的方法是会失败:

  1. 必须Run整个测试类(XXXTest.class)。
  2. 可以将@PrepareForTest(XXX.class)放在当前测试类的顶部,如果测试类中需要有多个PrepareForTest 可以以逗号隔开进行添加,如下图所示:
 

@RunWith(PowerMockRunner.class)

@PrepareForTest({Utils.class, Presenter.class})

public class UtilsTest {}

示例

初始化注入方式:

现在我们mock一个对象有四种方式,分别是普通方式、注解方式、运行器方法、MockitoRule方法。

推荐使用一,二种方式。

  1. 普通方式:
import org.junit.Assert;

import org.junit.Test;

import org.powermock.api.mockito.PowerMockito;

import java.util.ArrayList;

public class MockitoTest {

@Test

public void testNotNull() {

ArrayList arrayList = PowerMockito.mock(ArrayList.class);

Assert.assertNotNull(mArrayList);

}

}
  1. 注解方式
 
import org.junit.Assert;

import org.junit.Before;

import org.junit.Test;

import org.mockito.Mock;

import org.mockito.MockitoAnnotations;

import java.util.ArrayList;

public class MockitoAnnotationsTest {

@Mock

private ArrayList mArrayList;

@Before

public void setup() {

MockitoAnnotations.initMocks(this);

}

@Test

public void testNotNull() {

Assert.assertNotNull(mArrayList);

}

}
  1. 运行器方法:(android中,运行器使用Robolectric,该用法不可使用)
 
import org.junit.Test;

import org.junit.runner.RunWith;

import org.mockito.Mock;

import org.mockito.junit.MockitoJUnitRunner;

import java.util.ArrayList;

import static org.junit.Assert.assertNotNull;

@RunWith(PowerMockRunner.class)

public class MockitoJUnitRunnerTest {

@Mock //<--使用@Mock注解

ArrayList mArrayList;

@Test

public void testIsNotNull(){

assertNotNull(mArrayList);

}

}
  1. MockitoRule方法
 
import java.util.ArrayList;

import static org.junit.Assert.assertNotNull;

public class MockitoRuleTest {

@Mock //<--使用@Mock注解

ArrayList mArrayList;

@Rule //<--使用@Rule

public MockitoRule mockitoRule = MockitoJUnit.rule();

@Test

public void testIsNotNull(){

assertNotNull(mArrayList);

}

}

验证某些行为:

 
@Test

public void testListIsAdd() throws Exception {

mArrayList = PowerMockito.mock(ArrayList.class);

//使用mock对象执行方法

mArrayList.add("one");

mArrayList.clear();

//检验方法是否调用

verify(mArrayList).add("one");

verify(mArrayList).clear();

}

可以直接调用mock对象的方法,比如ArrayList.add()或者ArrayList.clear(),然后我们通过verify函数进行校验。

参数匹配器:

 
import org.mockito.ArgumentMatcher;

import java.util.List;

public class ListOfTwoElements implements ArgumentMatcher<List> {

public boolean matches(List list) {

return list.size() == 2;

}

public String toString() {

//printed in verification errors

return "[list of 2 elements]";

}

}


@Test

public void testArgumentMatchers() throws Exception {

mArrayList = PowerMockito.mock(ArrayList.class);

when(mArrayList.get(anyInt())).thenReturn("不管请求第几个参数 我都返回这句");

Assert.assertEquals("不管请求第几个参数 我都返回这句", mArrayList.get(0));

Assert.assertEquals("不管请求第几个参数 我都返回这句", mArrayList.get(39));

//当mockList调用addAll()方法时,「匹配器」如果传入的参数list size==2,返回true;

when(mArrayList.addAll(argThat(getListMatcher()))).thenReturn(true);

//根据API文档,我们也可以使用lambda表达式: 「匹配器」如果传入的参数list size==3,返回true;

// when(mArrayList.addAll(argThat(list -> list.size() == 3))).thenReturn(true);

boolean b1 = mArrayList.addAll(Arrays.asList("one", "two"));

boolean b2 = mArrayList.addAll(Arrays.asList("one", "two", "three"));

verify(mArrayList).addAll(argThat(getListMatcher()));

Assert.assertTrue(b1);

Assert.assertTrue(!b2);

}

private ListOfTwoElements getListMatcher() {

return new ListOfTwoElements();

}

对于一个Mock的对象,有时我们需要进行校验,但是基础的API并不能满足我们校验的需要,我们可以自定义Matcher,比如案例中,我们自定义一个Matcher,只有容器中两个元素时,才会校验通过。

验证方法的调用次数:

 
@Test

public void testVerifyTimes() throws Exception {

mArrayList = PowerMockito.mock(ArrayList.class);

mArrayList.add("once");

mArrayList.add("twice");

mArrayList.add("twice");

mArrayList.add("three times");

mArrayList.add("three times");

mArrayList.add("three times");

verify(mArrayList).add("once"); //验证mockList.add("once")调用了一次 - times(1) is used by default

verify(mArrayList, times(1)).add("once");//验证mockList.add("once")调用了一次

//调用多次校验

verify(mArrayList, times(2)).add("twice");

verify(mArrayList, times(3)).add("three times");

//从未调用校验

verify(mArrayList, never()).add("four times");

//至少、至多调用校验

verify(mArrayList, atLeastOnce()).add("three times");

verify(mArrayList, atMost(5)).add("three times");

}

抛出预期的异常:

 
@Test

public void testThrowNullPointerException() {

mArrayList = PowerMockito.mock(ArrayList.class);

doThrow(new NullPointerException("throwTest5.抛出空指针异常")).when(mArrayList).clear();

mArrayList.add("string");//这个不会抛出异常

mArrayList.clear();

}

@Test

public void testThrowIllegalArgumentException() {

mArrayList = PowerMockito.mock(ArrayList.class);

doThrow(new IllegalArgumentException("你的参数似乎有点问题")).when(mArrayList).add(anyInt());

mArrayList.add(12);//抛出了异常,因为参数是Int

}

校验方法执行顺序:

 
@Test

public void testListAddOrder() throws Exception {

List singleMock = mock(List.class);

singleMock.add("first add");

singleMock.add("second add");

InOrder inOrder = Mockito.inOrder(singleMock);

//inOrder保证了方法的顺序执行,如果顺序执行错误将failed

inOrder.verify(singleMock).add("first add");

inOrder.verify(singleMock).add("second add");

List firstMock = mock(List.class);

List secondMock = mock(List.class);

firstMock.add("first add");

secondMock.add("second add");

InOrder inOrder1 = Mockito.inOrder(firstMock, secondMock);

//下列代码会确认是否firstMock优先secondMock执行add方法

inOrder1.verify(firstMock).add("first add");

inOrder1.verify(secondMock).add("second add");

}

有时候我们需要校验方法执行顺序的先后,如案例所示,inOrder对象会判断方法执行顺序,如果顺序不对,该测试案例failed。

方法连续调用:

 
@Test

public void testContinueMethod() throws Exception {

Person person = mock(Person.class);

when(person.getName())

.thenReturn("第一次调用返回")

.thenThrow(new RuntimeException("方法调用第二次抛出异常"))

.thenReturn("第三次调用返回");

//另外一种方式

// when(person.getName()).thenReturn("第一次调用返回", "第二次调用返回", "第三次调用返回");

String name1 = person.getName();

String name2 = "";

try {

name2 = person.getName();

} catch (Exception e) {

name2 = e.getMessage();

}

String name3 = person.getName();

Assert.assertEquals("第一次调用返回", name1);

Assert.assertEquals("方法调用第二次抛出异常", name2);

// Assert.assertEquals("第二次调用返回", name2);

Assert.assertEquals("第三次调用返回", name3);

}

回调方法测试 thenAnswer:

 
@Test

public void testCallBack() throws Exception {

mArrayList = PowerMockito.mock(ArrayList.class);

when(mArrayList.add(anyString())).thenAnswer(new Answer<Boolean>() {

@Override

public Boolean answer(InvocationOnMock invocation) throws Throwable {

Object[] args = invocation.getArguments();

Object mock = invocation.getMock();

return false;

}

});

boolean first = mArrayList.add("第1次返回false");

Assert.assertFalse(first);

// lambda表达式

when(mArrayList.add(anyString())).then(invocation -> true);

boolean second = mArrayList.add("第2次返回true");

Assert.assertFalse(second);

when(mArrayList.add(anyString())).thenReturn(false);

boolean three = mArrayList.add("第3次返回false");

Assert.assertFalse(three);

}

Spy:监控真实对象

 
@Test

public void testSpyList() throws Exception {

List list = new ArrayList();

List spyList = PowerMockito.spy(list);//会创建真实对象

//当spyList调用size()方法时,return100

when(spyList.size()).thenReturn(100);

spyList.add("one");

String position0 = (String) spyList.get(0);

int size = spyList.size();

Assert.assertEquals("one", position0);

Assert.assertTrue(size == 100);

//下面这行代码会报错! java.lang.IndexOutOfBoundsException: 因为真实的只添加了一条数据

// String position1 = (String) spyList.get(1);

verify(spyList).add("one");

verify(spyList).size();

/*

* 请注意!下面这行代码会报错! java.lang.IndexOutOfBoundsException: Index: 10, Size: 2

不可能 : 因为当调用spy.get(0)时会调用真实对象的get(0)函数,此时会发生异常,因为真实List对象是空的

* */

// when(spyList.get(10)).thenReturn("ten");

//应该这么使用

doReturn("ten").when(spyList).get(9);

String position10 = (String) spyList.get(9);

Assert.assertEquals("ten", position10);

//Mockito并不会为真实对象代理函数调用,实际上它会拷贝真实对象。因此如果你保留了真实对象并且与之交互

//不要期望从监控对象得到正确的结果。当你在监控对象上调用一个没有被stub的函数时并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果。

//因此结论就是 : 当你在监控一个真实对象时,你想在stub这个真实对象的函数,那么就是在自找麻烦。或者你根本不应该验证这些函数。

}

Mock 参数传递的对象:

测试对象

 
public class MethodUtils {

public boolean callArgumentInstance(File file) {

return file.exists();

}

}

测试代码:

 
public class MethodUtilsTest {

@Test

public void testCallArgumentInstance() {

// Mock 对象,也可以使用 org.mockito.Mock 注解标记来实现

File file = PowerMockito.mock(File.class);

MethodUtils methodUtils = new MethodUtils();

// 录制 Mock 对象行为

PowerMockito.when(file.exists()).thenReturn(true);

// 验证方法行为

Assert.assertTrue(methodUtils.callArgumentInstance(file));

}

}

Mock 方法内部 new 出来的对象:

测试对象

 
import java.io.File;

public class CreateDirUtil {

public boolean createDirectoryStructure(String directoryPath) {

File directory = new File(directoryPath);

if (directory.exists()) {

String msg = "\"" + directoryPath + "\" 已经存在.";

throw new IllegalArgumentException(msg);

}

return directory.mkdirs();

}

}

测试用例

 
// 必须加注解 @PrepareForTest 和 @RunWith

@RunWith(PowerMockRunner.class)

public class CreateDirUtilTest {

@Test

@PrepareForTest(CreateDirUtil.class)

public void testCreateDirectoryStructureWhenPathDoesntExist() throws Exception {

final String directoryPath = "seemygod";

//创建File的模拟对象

File directoryMock = mock(File.class)

;

//在当前测试用例下,当出现new File("seemygod")时,就返回模拟对象

PowerMockito.whenNew(File.class).withArguments(directoryPath).thenReturn(directoryMock);

//当调用模拟对象的exists时,返回false

when(directoryMock.exists()).thenReturn(false);

//当调用模拟对象的mkdirs时,返回true

when(directoryMock.mkdirs()).thenReturn(true);

assertTrue(new CreateDirUtil().createDirectoryStructure(directoryPath));

//验证new File(directoryPath); 是否被调用过

verifyNew(File.class).withArguments(directoryPath);

}

}

Mock 普通对象的 final 方法:

测试对象

 
public class MethodUtils {

public boolean callFinalMethod(MethodDependency methodDependency) {

return methodDependency.isAlive();

}

}


public class MethodDependency {

public final boolean isAlive() {

// do something

return false;

}

}

测试用例

 
// 必须加注解 @PrepareForTest 和 @RunWith

@RunWith(PowerMockRunner.class)

public class MethodUtilsTest {

@Test

// 在测试方法之上加注解 @PrepareForTest,注解里写的类是需要 Mock 的 final 方法所在的类。

@PrepareForTest(MethodDependency.class)

public void testCallFinalMethod() {

MethodDependency methodDependency = PowerMockito.mock(MethodDependency.class);

MethodUtils methodUtils = new MethodUtils();

PowerMockito.when(methodDependency.isAlive()).thenReturn(true);

Assert.assertTrue(methodUtils.callFinalMethod(methodDependency));

}

}

Mock 静态方法:

测试代码

 
public class MethodUtils {

public boolean callStaticMethod() {

return MethodDependency.isExist();

}

}


public class MethodDependency {

public static boolean isExist() {

// do something

return false;

}

}

测试用例

 
// 必须加注解 @PrepareForTest 和 @RunWith

@RunWith(PowerMockRunner.class)

public class MethodUtilsTest {

@Test

// 在测试方法之上加注解 @PrepareForTest,注解里写的类是需要 Mock 的 static 方法所在的类。

@PrepareForTest(MethodDependency.class)

public void testCallStaticMethod() {

MethodUtils methodUtils = new MethodUtils();

// 表示需要 Mock 这个类里的静态方法

PowerMockito.mockStatic(MethodDependency.class);

PowerMockito.when(MethodDependency.isExist()).thenReturn(true);

Assert.assertTrue(methodUtils.callStaticMethod());

}

}

Mock 私有方法:

测试代码

 
public class MethodUtils {

public boolean callPrivateMethod() {

return isExist();

}

private boolean isExist() {

return false;

}

}

测试用例

 
// 必须加注解 @PrepareForTest 和 @RunWith

@RunWith(PowerMockRunner.class)

public class MethodUtilsTest {

@Test

// 在测试方法之上加注解 @PrepareForTest,注解里写的类是需要 Mock 的 private 方法所在的类。

@PrepareForTest(MethodUtils.class)

public void testCallPrivateMethod() throws Exception {

MethodUtils methodUtils = PowerMockito.mock(MethodUtils.class);

PowerMockito.when(methodUtils.callPrivateMethod()).thenCallRealMethod();

PowerMockito.when(methodUtils, "isExist").thenReturn(true);

Assert.assertTrue(methodUtils.callPrivateMethod());

}

}

Mock JDK 中 System 类的静态、私有方法:

测试代码

 
public class MethodUtils {

public boolean callSystemFinalMethod(String str) {

return str.isEmpty();

}

public String callSystemStaticMethod(String str) {

return System.getProperty(str);

}

}

测试用例

 
// 必须加注解 @PrepareForTest 和 @RunWith

@RunWith(PowerMockRunner.class)

public class MethodUtilsTest

@Test

// 和 Mock 普通对象的 static、final 方法一样,只不过注解 @PrepareForTest 里写的类不一样

// 注解里写的类是需要调用系统方法所在的类。

@PrepareForTest(MethodUtils.class)

public void testCallSystemFinalMethod() {

String str = PowerMockito.mock(String.class);

MethodUtils methodUtils = new MethodUtils();

PowerMockito.when(str.isEmpty()).thenReturn(false);

Assert.assertFalse(methodUtils.callSystemFinalMethod(str));

}

@Test

@PrepareForTest(MethodUtils.class)

public void testCallSystemStaticMethod() {

MethodUtils methodUtils = new MethodUtils();

PowerMockito.mockStatic(System.class);

PowerMockito.when(System.getProperty("aaa")).thenReturn("bbb");

Assert.assertEquals("bbb", methodUtils.callSystemStaticMethod("aaa"));

}

}

Mock 依赖类中的方法(whenNew):

 
public class MethodUtils {

public boolean callDependency() {

MethodDependency methodDependency = new MethodDependency();

return methodDependency.isGod("hh");

}

}


public class MethodDependency {

public boolean isGod(String oh){

System.out.println(oh);

return false;

}

}

测试用例

 
// 必须加注解 @PrepareForTest 和 @RunWith

@RunWith(PowerMockRunner.class)

public class TestClassUnderTest {

@Test

// 注解里写的类是依赖类所在的类。

@PrepareForTest(MethodUtils.class)

public void testDependency() throws Exception {

MethodUtils methodUtils = new MethodUtils();

MethodDependency methodDependency = mock(MethodDependency.class);

whenNew(MethodUtils.class).withAnyArguments().thenReturn(methodDependency);

when(methodDependency.isGod(anyString())).thenReturn(true);

Assert.assertTrue(methodUtils.callDependency());

}

}

关于调用自身的静态私有方法

有时候我们会调用到测试类自己的私有方法,例如现在有一个类FileUtils,我们要测试它的readFile2List()方法,代码如下。

 
/**

* Return the lines in file.

*

* @param file The file.

* @param st The line's index of start.

* @param end The line's index of end.

* @param charsetName The name of charset.

* @return the lines in file

*/

public static List<String> readFile2List(final File file,

final int st,

final int end,

final String charsetName) {

if (!isFileExists(file)) return null;

if (st > end) return null;

BufferedReader reader = null;

...

return null;

}

private static boolean isFileExists(final File file) {

return file != null && file.exists();

}

在需要测试的方法中调用了自己的isFileExists()函数,该函数的返回会影响到整个测试方法的结果,所以我们需要对该方法执行时给定一个结果(当然我们也可以用when方法使file.exists()返回给定结果,该方案不在本例范围内)

 
@Test

@PrepareForTest({FileIOUtils.class})

public void testGetFileByPath() throws Exception {

FileIOUtils utils = mock(FileIOUtils.class);

File file = mock(File.class);

//我们可以通过Whitebox调用自身的隐私方法

when(Whitebox.invokeMethod(utils, "isFileExists", any(String.class))).thenReturn(false);

assertNull(FileIOUtils.readFile2List(file));

}

Dao mock示例:

entity以及Dao接口:

 
public class User {

private Long id;

private String name;

public Long getId() {

return id;

}

public void setId(Long id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

}


public interface UserService {

/**

* 创建新用戶

*/

void createNewUser(User user) throws Exception;

}


public class UserServiceImpl implements UserService {

private UserDao mUserDao;

public void createNewUser(User user) throws Exception {

// 参数校验

if (user == null || user.getId() == null || isEmpty(user.getName())) {

throw new IllegalArgumentException();

}

// 查看是否是重复数据

Long id = user.getId();

User dbUser = mUserDao.queryUser(id);

if (dbUser != null) {

throw new Exception("用户已经存在");

}

try {

mUserDao.insertUser(dbUser);

} catch (Exception e) {

// 隐藏Database异常,抛出服务异常

throw new Exception("数据库语句执行失败", e);

}

}

private boolean isEmpty(String str) {

if (str == null || str.trim().length() == 0) {

return true;

}

return false;

}

public void setUserDao(UserDao userDao) {

this.mUserDao = userDao;

}

}

测试用例:

 
import org.junit.Test;

import org.mockito.invocation.InvocationOnMock;

import org.mockito.stubbing.Answer;

import static org.mockito.Mockito.*;

import java.sql.SQLException;

public class UserServiceImplTest {

@Test(expected = IllegalArgumentException.class)

public void testNullUser() throws Exception {

UserService userService = new UserServiceImpl();

// 创建mock

UserDao userDao = mock(UserDao.class);

((UserServiceImpl) userService).setUserDao(userDao);

userService.createNewUser(null);

}

@Test(expected = IllegalArgumentException.class)

public void testNullUserId() throws Exception {

UserService userService = new UserServiceImpl();

// 创建mock

UserDao userDao = mock(UserDao.class);

((UserServiceImpl) userService).setUserDao(userDao);

User user = new User();

user.setId(null);

userService.createNewUser(user);

}

@Test(expected = IllegalArgumentException.class)

public void testNullUserName() throws Exception {

UserService userService = new UserServiceImpl();

// 创建mock对象

UserDao userDao = mock(UserDao.class);

((UserServiceImpl) userService).setUserDao(userDao);

User user = new User();

user.setId(1L);

user.setName("");

userService.createNewUser(user);

}

@Test(expected = Exception.class)

public void testCreateExistUser() throws Exception {

UserService userService = new UserServiceImpl();

// 创建mock

UserDao userDao = mock(UserDao.class);

User returnUser = new User();

returnUser.setId(1L);

returnUser.setName("Vikey");

//指定行为

when(userDao.queryUser(1L)).thenReturn(returnUser);

((UserServiceImpl) userService).setUserDao(userDao);

User user = new User();

user.setId(1L);

user.setName("Vikey");

userService.createNewUser(user);

}

@Test(expected = Exception.class)

public void testCreateUserOnDatabaseException() throws Exception {

UserService userService = new UserServiceImpl();

// 创建mock

UserDao userDao = mock(UserDao.class);

//指定行为 调用后抛异常

doThrow(new SQLException("SQL is not valid")).when(userDao).insertUser(any(User.class));

((UserServiceImpl) userService).setUserDao(userDao);

User user = new User();

user.setId(1L);

user.setName("Vikey");

userService.createNewUser(user);

}

@Test

public void testCreateUser() throws Exception {

UserService userService = new UserServiceImpl();

// 创建mock

UserDao userDao = mock(UserDao.class);

//拦截行为

doAnswer(new Answer<Void>() {

public Void answer(InvocationOnMock invocation) throws Throwable {

System.out.println("Insert data into user table");

return null;

}

}).when(userDao).insertUser(any(User.class));

((UserServiceImpl) userService).setUserDao(userDao);

User user = new User();

user.setId(1L);

user.setName("Vikey");

userService.createNewUser(user);

}

}

Robolectric github issues

普通的AndroidJunit测试需要跑到设备或模拟器上去,需要打包apk运行,这样速度很慢,相当于每次运行app一样。而Robolectric通过实现一套能运行的Android代码的JVM环境,然后在运行unit test的时候去截取android相关的代码调用,然后转到自己实现的代码去执行这个调用的过程,从而达到能够脱离Android环境运行Android测试代码的目的。

添加配置和依赖:

 
android {

//使用robolectric必须配置

testOptions {

unitTests {

includeAndroidResources = true

}

}

}


dependencies {

testImplementation 'org.robolectric:robolectric:4.4'

}

如工程遇到以下问题需添加此依赖:

 
testImplementation ('org.bouncycastle:bcprov-jdk15on:1.64') {

force = true

}

在项目根目录下的gradle.properties文件中添加下面的配置(在Android Studio 3.3+以上不需要):

 

android.enableUnitTestBinaryResources=true

我这里是依赖的Robolectric 4.4版本的,是目前最新版本,对Android Gradle Plugin / Android Studio 的要求是 3.2或者更新,后续如果有版本更新,可以参考官方的《配置迁移指南》。

测试类配置:

 
@RunWith(RobolectricTestRunner.class)

@Config(constants = BuildConfig.class, sdk = {Build.VERSION_CODES.P})

public class MainActivityTest {

}
  1. Config配置

在Robolectric当中你可以通过@Config注解来配置一些跟Android相关的系统配置

配置SDK版本

Robolectric会根据manifest文件配置的targetSdkVersion选择运行测试代码的SDK版本,如果你想指定sdk来运行测试用例,可以通过下面的方式配置:

 
@Config(sdk = Build.VERSION_CODES.P)

public class SandwichTest {

@Config(sdk = Build.VERSION_CODES.KITKAT)

public void testGetSandwich_shouldReturnHamSandwich() {

}

}

配置Application类

Robolectric会根据manifest文件配置的Application配置去实例化一个Application类,如果你想在测试用例中重新指定,可以通过下面的方式配置:

 
@Config(application = CustomApplication.class)

public class SandwichTest {

@Config(application = CustomApplicationOverride.class)

public void testGetSandwich_shouldReturnHamSandwich() {

}

}

我们在单元测试的时候需要给Robolectric单独实现一个application,因为在实际的Application类的oncreate()方法中我们会去初始化第三方的库,这可能导致运行测试方法报错,比如有些第三方会调用static{ Library.load() }静态加载so库等。为测试类配置一个空的Application类,通过@Config指定:

 
public class RobolectricApp extends Application {

@Override

public void onCreate() {

super.onCreate();

}

}
 

@Config(application = RobolectricApp.class)

指定Resource路径

Robolectric可以让你配置manifest、resource和assets路径,可以通过下面的方式配置:

 
@Config(manifest = "some/build/path/AndroidManifest.xml",

assetDir = "some/build/path/assetDir",

resourceDir = "some/build/path/resourceDir")

public class SandwichTest {

@Config(manifest = "other/build/path/AndroidManifest.xml")

public void testGetSandwich_shouldReturnHamSandwich() {

}

}

使用第三方Library Resources

当Robolectric测试的时候,会尝试加载所有应用提供的资源,但如果你需要使用第三方库中提供的资源文件,你可能需要做一些特别的配置。不过如果你使用gradle来构建Android应用,这些配置就不需要做了,因为Gradle Plugin会在build的时候自动合并第三方库的资源,但如果你使用的是Maven,那么你需要配置libraries变量:

 
@RunWith(RobolectricTestRunner.class)

@Config(libraries = {

"build/unpacked-libraries/library1",

"build/unpacked-libraries/library2"

})

public class SandwichTest {

}

使用限定的资源文件

Android会在运行时加载特定的资源文件,如根据设备屏幕加载不同分辨率的图片资源、根据系统语言加载不同的string.xml,在Robolectric测试当中,你也可以进行一个限定,让测试程序加载特定资源.多个限定条件可以用破折号拼接在在一起。

 
/**

* 使用qualifiers加载对应的资源文件

*/

@Config(qualifiers = "zh-rCN")

@Test

public void testString() throws Exception {

final Context context = ApplicationProvider.getApplicationContext();

assertThat(context.getString(R.string.app_name), is("单元测试Demo"));

}

可参考:Using Qualified Resources

我们可以在Config类的源码中看到支持的哪些属性配置:

通过Properties文件配置

如果你嫌通过注解配置上面的东西麻烦,你也可以把以上配置放在一个Properties文件之中,然后通过@Config指定配置文件,比如,首先创建一个配置文件robolectric.properties:

 
# 放置Robolectric的配置选项:

sdk=21

manifest=some/build/path/AndroidManifest.xml

assetDir=some/build/path/assetDir

resourceDir=some/build/path/resourceDir

然后把robolectric.properties文件放到src/test/resources目录下,运行的时候,会自动加载里面的配置

系统属性配置

 

robolectric.offline:true代表关闭运行时获取jar包

robolectric.dependency.dir:当处于offline模式的时候,指定运行时的依赖目录

robolectric.dependency.repo.id:设置运行时获取依赖的Maven仓库ID,默认是sonatype

robolectric.dependency.repo.url:设置运行时依赖的Maven仓库地址,默认是https://oss.sonatype.org/content/groups/public/

robolectric.logging.enabled:设置是否打开调试开关

以上设置可以通过Gradle进行配置,如:

 
android {

testOptions {

unitTests.all {

systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'

systemProperty 'robolectric.dependency.repo.id', 'local'

}

}

}

设备配置

可参考:Device Configuration

注意,从Roboelectric3.3开始,测试运行程序将在classpath中查找名为/com/android/tools/test_config.properties的文件。如果找到它,它将用于为测试提供默认manifest, resource, 和 asset 资源文件的位置,而无需在测试中指定@config(constants=buildconfig.class)或@config(manifest=…“,res=…”,assets=…“)。另外,Roboelectric在运行单元测试方法时,必须确保构R.class已经构建生成。

常用测试场景

  1. 获取上下文菜单

 

//第一种方式 已过时

Context context = RuntimeEnvironment.application;

//第二种方式

Context context2 = ApplicationProvider.getApplicationContext();

  1. 验证Activity页面跳转

 
public class MainActivity extends Activity implements View.OnClickListener {

private final static String TAG = MainActivity.class.getSimpleName();

private Button mLoginBtn;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

initView();

}

private void initView() {

mLoginBtn = (Button) findViewById(R.id.btn_login);

mLoginBtn.setOnClickListener(this);

}

@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.btn_login:

Intent intent = new Intent(this, LoginActivity.class);

startActivity(intent);

break;

default:

break;

}

}

}

然后在测试类中添加测试方法对点击事件进行测试:

 
@RunWith(RobolectricTestRunner.class)

@Config(shadows = {ShadowLog.class}, sdk = {Build.VERSION_CODES.P})

@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})

@PrepareForTest({MainActivity.class})

public class MainActivityTest {

@Before

public void setUp(){

//输出日志配置,用System.out代替Android的Log.x

ShadowLog.stream = System.out;

}

@Test

public void testOnClick() {

//创建Activity

MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

Assert.assertNotNull(mainActivity);

//模拟点击

mainActivity.findViewById(R.id.btn_login).performClick();

// 获取对应的Shadow类

ShadowActivity shadowActivity = shadowOf(activity);

// 借助Shadow类获取启动下一Activity的Intent

Intent nextIntent = shadowActivity.getNextStartedActivity();

// 校验Intent的正确性

assertEquals(nextIntent.getComponent().getClassName(), LoginActivity.class.getName());

}

}

其中@RunWith(RobolectricTestRunner.class)指定Robolectric运行器,不用多说了。

@Config(shadows = {ShadowLog.class}, sdk = sdk = {Build.VERSION_CODES.P})通过配置shadows = {ShadowLog.class}和在@Before函数中指定ShadowLog.stream = System.out是为了用java的System.out代替Android的Log输出,这样就能在run时的控制台看到Android的日志输出了。

@PowerMockIgnore({"org.mockito.", "org.robolectric.", "android.", "org.json.", "sun.security.", "javax.net."})通过PowerMockIgnore注解定义所忽略的package路径,防止所定义的package路径下的class类被PowerMockito测试框架mock。

在setUp()方法中调用MockitoAnnotations.initMocks(this);初始化PowerMockito注解,为@PrepareForTest(YourStaticClass.class)注解提供支持。

Robolectric.setupActivity是用来创建Activity, 当 Robolectric.setupActivity返回的时候,默认会调用Activity的生命周期: onCreate -> onStart -> onResume。

前面说过目标MainActivity中是点击按钮跳到一个LoginActivity, 为了测试这一点,我们可以检查当用户单击“登录”按钮时,是否启动了正确的Intent。因为Roboelectric是一个单元测试框架,实际上并不会真正的去启动MainActivity,但是我们可以检查MainActivity是否触发了正确的Intent,以达到验证目的。

右键去运行testOnClick()方法:

  1. 验证Toast显示

同样是上面的代码,我们点击按钮时弹出一个Toast然后去测试Toast是否已经显示:

 
@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.btn_login:

Toast.makeText(this, "测试", Toast.LENGTH_SHORT).show();

break;

default:

break;

}

}

测试类:

 
@Test

public void testToast() {

MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

mainActivity.findViewById(R.id.btn_login).performClick();

// 判断Toast已经弹出

assertNotNull(ShadowToast.getLatestToast());

//验证捕获的最近显示的Toast

Assert.assertEquals("测试", ShadowToast.getTextOfLatestToast());

//捕获所有已显示的Toast

List<Toast> toasts = shadowOf(ApplicationProvider.getApplicationContext()).getShownToasts();

Assert.assertThat(toasts.size(), is(1));

Assert.assertEquals(Toast.LENGTH_SHORT, toasts.get(0).getDuration());

}

关于Shadow

前面的代码中都会出现一个shadow的关键词,Roboelectric通过一套测试API扩展了Android framework,这些API提供了额外的可配置性,并提供了对测试有用的Android组件的内部状态和历史的访问性。这种访问性就是通过Shadow类(影子类)来实现的,许多测试API都是对单个Android类的扩展,你可以使用Shadows.shadowOf()方法访问。

Roboelectric几乎针对所有的Android组件提供了一个Shadow开头的类,例如ShadowActivity、ShadowDialog、ShadowToast、ShadowApplication等等。Robolectric通过这些Shadow类来模拟Android系统的真实行为,当这些Android系统类被创建的时候,Robolectric会查找对应的Shadow类并创建一个Shadow类对象与原始类对象关联。每当系统类的方法被调用的时候,Robolectric会保证Shadow对应的方法会调用。每个Shadow对象都可以修改或扩展Android操作系统中相应类的行为。因此我们可以用Shadow类的相关方法对Android相关的对象进行测试。

更多关于Shadow的知识请参考官方介绍:Shadows 

  1. 验证Dialog显示

 
@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.btn_login:

showDialog();

break;

default:

break;

}

}

public void showDialog(){

AlertDialog alertDialog = new AlertDialog.Builder(this)

.setMessage("测试showDialog")

.setTitle("提示")

.create();

alertDialog.show();

}

测试类:


@Test

public void testShowDialog() {

MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

// 捕获最近显示的Dialog

AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();

// 判断Dialog尚未弹出

Assert.assertNull(dialog);

//点击按钮

mainActivity.findViewById(R.id.btn_login).performClick();

// 捕获最近显示的Dialog

dialog = ShadowAlertDialog.getLatestAlertDialog();

// 判断Dialog已经弹出

Assert.assertNotNull(dialog);

// 获取Shadow类进行验证

ShadowAlertDialog shadowDialog = Shadows.shadowOf(dialog);

Assert.assertEquals("测试showDialog", shadowDialog.getMessage());

}
  1. 验证UI组件状态

以CheckBox为例:

 
@Test

public void testCheckBoxState() throws Exception {

MainActivity activity = Robolectric.setupActivity(MainActivity.class);

CheckBox checkBox = activity.findViewById(R.id.checkbox);

// 验证CheckBox初始状态

Assert.assertFalse(checkBox.isChecked());

// 点击按钮反转CheckBox状态

checkBox.performClick();

// 验证状态是否正确

Assert.assertTrue(checkBox.isChecked());

// 点击按钮反转CheckBox状态

checkBox.performClick();

// 验证状态是否正确

Assert.assertFalse(checkBox.isChecked());

}
  1. 访问资源文件

使用ApplicationProvider.getApplicationContext()可以获取到Application对象,方便我们使用。比如访问资源文件。

 
@Test

public void testResource() throws Exception {

Application application = ApplicationProvider.getApplicationContext();

String appName = application.getString(R.string.app_name);

Assert.assertEquals("AndroidUnitTestApplication", appName);

}
  1. 验证Intent参数传递

 
@Test

public void testStartActivityWithIntent() throws Exception {

Intent intent = new Intent();

intent.putExtra("test", "HelloWorld");

Activity activity = Robolectric.buildActivity(MainActivity.class, intent).create().get();

Bundle extras = activity.getIntent().getExtras();

assertNotNull(extras);

assertEquals("HelloWorld", extras.getString("test"));

}
  1. 验证BroadcastReceiver

我们先在Activity中注册一个广播:

 
public class MainActivity extends Activity {

private final static String TAG = MainActivity.class.getSimpleName();

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

public static final String ACTION_TEST = "com.fly.unit.test";

public static final String ACTION_TEST2 = "com.fly.unit.test2";

private BroadcastReceiver mTestBroadcastReceiver = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

Log.e(TAG, "mTestBroadcastReceiver onReceive: " + intent.getAction());

if (ACTION_TEST.equals(intent.getAction())) {

String name = intent.getStringExtra("name");

PreferenceManager.getDefaultSharedPreferences(context)

.edit()

.putString("name", name)

.apply();

}

}

};

@Override

protected void onResume() {

super.onResume();

IntentFilter intentFilter = new IntentFilter(ACTION_TEST);

intentFilter.addAction(ACTION_TEST2);

LocalBroadcastManager.getInstance(this)

.registerReceiver(mTestBroadcastReceiver, intentFilter);

}

@Override

protected void onPause() {

super.onPause();

LocalBroadcastManager.getInstance(this).unregisterReceiver(mTestBroadcastReceiver);

}

}

我们使用LocalBroadcastManageronResume()方法中注册了拥有两个Action的广播,然后在onPause中反注册了这个广播。在onReceive方法中只针对ACTION_TEST这个action做了sp保存的操作。下面测试类进行验证

 
@Test

public void testBroadcastReceive() throws Exception {

MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

LocalBroadcastManager broadcastManager = ShadowLocalBroadcastManager.getInstance(mainActivity);

Intent intent = new Intent(MainActivity.ACTION_TEST);

intent.putExtra("name", "小明");

//发送广播

broadcastManager.sendBroadcast(intent);

SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mainActivity);

//通过验证sp保存的值验证广播是否收到

Assert.assertEquals("小明", preferences.getString("name", null));

intent = new Intent(MainActivity.ACTION_TEST2);

intent.putExtra("name", "小红");

//再次发送一个广播

broadcastManager.sendBroadcast(intent);

//验证新的参数值是否被保存(由于广播中我们没有对这个Action处理,因此sp中的name应该还是上次的)

Assert.assertNotEquals("小红", preferences.getString("name", null));

}

同时控制台也能看到log输出:

同样我们也可以通过控制生命周期验证广播是否被注销了,上面代码MainActivity是在onPause()方法中反注册了广播,如果我们忘记了反注册广播这一点,那么在onPause()方法之后发送广播应该还是会收到。

 
@Test

public void testBroadcastReceive2() throws Exception {

ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);

controller.setup();

controller.pause();

LocalBroadcastManager broadcastManager = ShadowLocalBroadcastManager.getInstance(RuntimeEnvironment.application);

broadcastManager.sendBroadcast(new Intent(MainActivity.ACTION_TEST));

//自行验证,例如可以看是否有log输出

}

验证静态广播是否注册:

 
<receiver android:name=".receiver.MyReceiver"

android:exported="false">

<intent-filter>

<action android:name="com.test.receiver.MyReceiver"/>

</intent-filter>

</receiver>


@Test

public void testBroadcastReceive3() {

Intent intent = new Intent("com.test.receiver.MyReceiver");

PackageManager packageManager = RuntimeEnvironment.application.getPackageManager();

List<ResolveInfo> resolveInfos = packageManager.queryBroadcastReceivers(intent, 0);

assertNotNull(resolveInfos);

assertThat(resolveInfos.size(), Matchers.greaterThan(0));

}
  1. 验证Service

跟Activity一样可以通过ServiceController验证Service的生命周期:

 
public class MyService extends Service {

private final String TAG = MyService.class.getSimpleName();

@Nullable

@Override

public IBinder onBind(Intent intent) {

Log.d(TAG, "onBind");

return null;

}

@Override

public void onCreate() {

super.onCreate();

Log.d(TAG, "onCreate");

}

@Override

public boolean onUnbind(Intent intent) {

Log.d(TAG, "onUnbind");

return super.onUnbind(intent);

}

@Override

public void onDestroy() {

super.onDestroy();

Log.d(TAG, "onDestroy");

}

@Override

public int onStartCommand(Intent intent, int flags, int startId) {

Log.d(TAG, "onStartCommand");

return super.onStartCommand(intent, flags, startId);

}

}

测试代码:

 
@Test

public void testServiceLifecycle() throws Exception {

//验证Service生命周期

ServiceController<MyService> controller = Robolectric.buildService(MyService.class);

controller.create();

// verify something

controller.startCommand(0, 0);

// verify something

controller.bind();

// verify something

controller.unbind();

// verify something

controller.destroy();

// verify something

}

控制台输出:

Robolectric.setupActivity一样,可以调用Robolectric.setupService直接创建一个Service实例:

 

MyService myService = Robolectric.setupService(MyService.class);

//verify somthing

但是Robolectric.setupService只会调用Service的onCreate()方法。

也可以像验证Activity的启动那样,验证在某个时刻是否启动了目标Service(如验证点击按钮启动一个Service):

 
@Test

public void testServiceCreate() {

final MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

mainActivity.findViewById(R.id.btn_login).performClick();

Intent intent = new Intent(mainActivity, MyService.class);

Intent actual = ShadowApplication.getInstance().getNextStartedService();

Assert.assertEquals(intent.getComponent(), actual.getComponent());

}

验证在Activity的onCreate方法中启动了Service:

 
@Test

public void testServiceCreate() {

ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);

MainActivity mainActivity = controller.create().get();

Intent intent = new Intent(mainActivity, MyService.class);

Intent actual = ShadowApplication.getInstance().getNextStartedService();

Assert.assertEquals(intent.getComponent(), actual.getComponent());

}

测试IntentService:


public class SampleIntentService extends IntentService {

public SampleIntentService() {

super("SampleIntentService");

}

@Override

protected void onHandleIntent(Intent intent) {

SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences(

"example", Context.MODE_PRIVATE).edit();

editor.putString("SAMPLE_DATA", "sample data");

editor.apply();

}

}

测试代码:

 
@Test

public void addsDataToSharedPreference() {

Application application = RuntimeEnvironment.application;

RoboSharedPreferences preferences = (RoboSharedPreferences) application

.getSharedPreferences("example", Context.MODE_PRIVATE);

Intent intent = new Intent(application, SampleIntentService.class);

SampleIntentService registrationService = new SampleIntentService();

registrationService.onHandleIntent(intent);

assertNotSame("", preferences.getString("SAMPLE_DATA", ""), "");

}
  1. 验证DelayedRunnable

我们在UI主线程有时会执行一些postDelayed Runnable操作,例如点击按钮时postDelayed一个Runnable来设置UI状态:

 
@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.btn_login:

mLoginBtn.postDelayed(new Runnable() {

@Override

public void run() {

mLoginBtn.setText("测试");

}

}, 500);

//或者类似这样的:

// new Handler().postDelayed(new Runnable() {

// @Override

// public void run() {

// mLoginBtn.setText("测试");

// }

// }, 500);

break;

default:

break;

}

}

这种操作虽然最终也会发生在UI主线程上进行,但是发生并不是即时的,如果你像之前一样使用下面的代码进行测试,则会验证失败:

 
@Test

public void testPostRunnable() {

MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

Assert.assertNotNull(mainActivity);

Button btn = mainActivity.findViewById(R.id.btn_login);

btn.performClick();

Assert.assertEquals("测试", btn.getText());

}

这时可以通过 ShadowLooper.runUiThreadTasksIncludingDelayedTasks()或者ShadowLooper.runMainLooperOneTask()方法使所有UI线程上的延时任务即刻发生,我们在performClick()方法之后调这个方法,然后就可以正常断言了:

 
@Test

public void testPostRunnable() {

MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

Assert.assertNotNull(mainActivity);

//模拟点击

Button btn = mainActivity.findViewById(R.id.btn_login);

btn.performClick();

ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

//使用下面这句也可以做到

//ShadowLooper.runMainLooperOneTask();

Assert.assertEquals("测试", btn.getText());

}

SQLite -- 单元测试

关于如何对SQLite数据库进行单元测试,我们使用本地单元测试 Robolectric + JUnit 对数据库进行测试。 这样做的原因是:Robolectric可以模拟Android的运行环境,让Android代码脱离手机/模拟器,直接运行在JVM上面,速度比在真机/模拟器上要快很多。

构建测试环境

以 Android Studio 为例,我们需要在 module-name/src/test/java/ 中进行测试。当你创建新项目时,此目录已存在。

在应用的顶级app build.gradle 文件中,请将以下库指定为依赖项:

 
testImplementation 'junit:junit:4.12'

testImplementation 'org.robolectric:robolectric:4.4'

如果出现以下错误,需添加下面依赖

 
testImplementation ('org.bouncycastle:bcprov-jdk15on:1.64') {

force = true

}

将以下代码行添加到同一 build.gradle 文件的 android{} 中:

 
android {

testOptions {

unitTests {

includeAndroidResources = true

}

}

}

Gradle 构建文件示例

 
apply plugin: 'com.android.application'

android {

testOptions {

unitTests {

includeAndroidResources = true

}

}

}

dependencies {

testImplementation 'junit:junit:4.12'

testImplementation 'org.robolectric:robolectric:4.4'

testImplementation ('org.bouncycastle:bcprov-jdk15on:1.64') {

force = true

}

}

示例:

DbTestHelper:

 
package com.smartisanos.filemanagerservice;

import android.content.Context;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteOpenHelper;

import androidx.annotation.Nullable;

public class DbTestHelper extends SQLiteOpenHelper {

private static final int DB_VERSION = 1;

public DbTestHelper(Context context, String dbName) {

this(context, dbName, DB_VERSION);

}

public DbTestHelper(Context context, String dbName, int dbVersion) {

this(context, dbName, null, dbVersion);

}

public DbTestHelper(@Nullable Context context, @Nullable String name,

@Nullable SQLiteDatabase.CursorFactory factory, int version) {

super(context, name, factory, version);

}

@Override

public void onCreate(SQLiteDatabase db) {

createLruTable(db);

}

@Override

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

}

public void createLruTable(SQLiteDatabase db) {

db.execSQL("CREATE TABLE IF NOT EXISTS TestBean ( id INTEGER PRIMARY KEY, name )");

}

}

TestBean:

 
package com.smartisanos.filemanagerservice;

public class TestBean {

private int mId;

private String mName = "";

public TestBean(int id, String name) {

this.mId = id;

this.mName = name;

}

public int getId() {

return mId;

}

public void setId(int id) {

this.mId = id;

}

public String getName() {

return mName;

}

public void setName(String name) {

this.mName = name;

}

}

TestDbDAO:

 
package com.smartisanos.filemanagerservice;

import android.content.ContentValues;

import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;

public class TestDbDAO {

private static boolean isTableExist;

private SQLiteDatabase db;

public TestDbDAO(SQLiteDatabase db) {

this.db = db;

}

public void closeDb() {

db.close();

}

/**

* insert TestBean

*/

public void insert(TestBean bean) {

checkTable();

ContentValues values = new ContentValues();

values.put("id", bean.getId());

values.put("name", bean.getName());

db.insert("TestBean", "", values);

}

/**

* 获取对应id的TestBean

*/

public TestBean get(int id) {

checkTable();

Cursor cursor = null;

try {

cursor = db.rawQuery("SELECT * FROM TestBean", null);

if (cursor != null && cursor.moveToNext()) {

String name = cursor.getString(cursor.getColumnIndex("name"));

return new TestBean(id, name);

}

} catch (Exception e) {

e.printStackTrace();

} finally {

if (cursor != null) {

cursor.close();

}

cursor = null;

}

return null;

}

/**

* 检查表是否存在,不存在则创建表

*/

private void checkTable() {

if (!isTableExist()) {

db.execSQL("CREATE TABLE IF NOT EXISTS TestBean ( id INTEGER PRIMARY KEY, name )");

}

}

private boolean isTableExist() {

if (isTableExist) {

return true; // 上次操作已确定表已存在于数据库,直接返回true

}

Cursor cursor = null;

try {

String sql = "SELECT COUNT(*) AS c FROM sqlite_master WHERE type ='table' AND name " +

"='TestBean' ";

cursor = db.rawQuery(sql, null);

if (cursor != null && cursor.moveToNext()) {

int count = cursor.getInt(0);

if (count > 0) {

isTableExist = true; // 记录Table已创建,下次执行isTableExist()时,直接返回true

return true;

}

}

} catch (Exception e) {

e.printStackTrace();

} finally {

if (cursor != null) {

cursor.close();

}

cursor = null;

}

return false;

}

}

RoboApp:

如果使用应用本身实现的BaseApplication,不利于单元测试。BaseApplication是项目本来的Application,但是使用Robolectric往往会指定一个测试专用的Application(命名为RoboApp),这么做好处是隔离App的所有依赖。如果用Robolectric单元测试,不配置RoboApp,就会调用原来的BaseApplication,而BaseApplication有很多第三方库依赖,常见的有static{ Library.load() }静态加载so库。于是,执行BaseApplication生命周期时,robolectric就报错了。

 
package com.smartisanos.filemanagerservice;

import android.app.Application;

public class RoboApp extends Application {

}

正确的使用方式是我们为Robolectric 单独实现一个 Application,使用方式是在单元测试XXTest加上@Config(application = RoboApp.class)

 
@RunWith(RobolectricTestRunner.class)

@Config(application = RoboApp.class)

public class SQLiteExampleTest {

}

SQLiteExampleTest:文章来源地址https://www.toymoban.com/news/detail-598881.html

 
package com.smartisanos.filemanagerservice;

import org.junit.After;

import org.junit.Assert;

import org.junit.Before;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.robolectric.RobolectricTestRunner;

import org.robolectric.RuntimeEnvironment;

import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)

@Config(manifest = Config.NONE, application = RoboApp.class)

public class SQLiteExampleTest {

private static final String DB_NAME = "lruFileTest.db";

private TestDbDAO dbDAO;

@Before

public void setUp() {

DbTestHelper dbHelper = new DbTestHelper(RuntimeEnvironment.application, DB_NAME);

dbDAO = new TestDbDAO(dbHelper.getWritableDatabase());

}

@Test

public void testInsertAndGet() {

dbDAO.insert(new TestBean(1, "键盘"));

TestBean retBean = dbDAO.get(1);

Assert.assertEquals(retBean.getId(), 1);

Assert.assertEquals(retBean.getName(), "键盘");

}

@After

public void closeDb() {

dbDAO.closeDb();

}

}

到了这里,关于自动化测试集成指南 -- 本地单元测试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • MIL自动化单元测试

    之前学习MIL, 一直想对模型的进行自动化测试,最近正好做了,把心得写下来。 MIL测试就是模型在环测试,通过纯软件仿真的形式,验证模型能否满足功能需求,尽早发现问题。 MIL分为单元测试与集成测试 看图很好理解,集成测试闭环,单元测试开环。 确定被测模型的 输入

    2023年04月09日
    浏览(49)
  • Android 自动化单元测试

    2024年02月13日
    浏览(42)
  • Diffblue Cover AI Java:Difflane如何利用Diffblue Cover AI实现Java自动化的单元测试(Diffblue Cover快速入门

    三、如何使用 1、Diffblue Cover:下载链接https://www.diffblue.com/community-edition/download 2、关于Cover IntelliJ插件 有两个Diffblue IntelliJ插件-完整的Cover IntelliJ插件和Cover Community Edition IntelliJ插件。Cover Community Edition是免费的,可以用于开放源代码项目;Cover的完整(收费)版本可用于任何

    2024年04月11日
    浏览(43)
  • Pytest自动化测试框架---(单元测试框架)

    unittest是python自带的单元测试框架,它封装好了一些校验返回的结果方法和一些用例执行前的初始化操作,使得单元测试易于开展,因为它的易用性,很多同学也拿它来做功能测试和接口测试,只需简单开发一些功能(报告,初始化webdriver,或者http请求方法)便可实现。 但自

    2024年02月14日
    浏览(70)
  • 自动化测试之JUnit单元测试框架

    目录 一、什么是 JUnit 二、JUnit5 相关技术 1.注解 1.1 @Test 1.2 @Disabled 1.3 @BeforeAll、@AfterAll 1.4 @BeforeEach、@AfterEach 2.参数化 2.1 单参数 2.2 CSV 获取参数 2.3 方法获取参数 2.4 多参数 3.测试用例的执行顺序 3.1 顺序执行:@TestMethodOrder(MethodOrderer.OrderAnnotation.class) 3.2 随机执行:@TestMetho

    2024年02月06日
    浏览(80)
  • 软件测试之单元测试自动化入门基础

    所谓的单元测试(Unit Test)是根据特定的输入数据,针对程序代码中的最小实体单元的输入输出的正确性进行验证测试的过程。所谓的最小实体单元就是组织项目代码的最基本代码结构: 函数,类,模块 等。在Python中比较知名的单元测试模块: unittest pytest doctest nose 所谓的测试

    2024年02月03日
    浏览(49)
  • 【云原生持续交付和自动化测试】5.2 自动化测试和集成测试

    往期回顾: 第一章:【云原生概念和技术】 第二章:【容器化应用程序设计和开发】 第三章:【基于容器的部署、管理和扩展】 第四章:【微服务架构设计和实现】 第五章:【5.1 自动化构建和打包容器镜像】 5.2.1 什么是自动化测试和集成测试? 云原生的自动化测试和集

    2024年02月14日
    浏览(64)
  • 自动化测试、压力测试、持续集成

    因为项目的原因,前段时间研究并使用了 SoapUI 测试工具进行自测开发的 api。下面将研究的成果展示给大家,希望对需要的人有所帮助。 SoapUI 是一个开源测试工具,通过 soap/http 来检查、调用、实现 Web Service 的功能/负载/符合性测试。该工具既可作为一个单独的测试软件使

    2024年02月04日
    浏览(64)
  • 持续集成——web自动化测试集成实战

    减少错误和手动任务 及早发现并解决集成挑战 更短的交付周期 被测代码(存放于代码仓) Jenkins节点机器以及运行环境 博客地址:https://blog.csdn.net/YZL40514131/article/details/130142810?spm=1001.2014.3001.5501 当前项目在windows节点跑,所以需要在windows中配置各种环境变量 Chrome 浏览器和chr

    2023年04月24日
    浏览(49)
  • 持续集成——App自动化测试集成实战

    1、减少错误和手动任务 2、及早发现并解决集成挑战 3、更短的交付周期 1、被测代码(存放于代码仓) 2、Jenkins节点机器以及运行环境 博客地址:https://blog.csdn.net/YZL40514131/article/details/130142810?spm=1001.2014.3001.5501 Pycharm拉取代码执行 命令行运行代码,并生成报告 pip freeze require

    2024年02月01日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包