Android下单元测试实践——测试框架简介

这篇具有很好参考价值的文章主要介绍了Android下单元测试实践——测试框架简介。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

android单元测试private变量,软件测试,单元测试,软件测试,功能测试,自动化测试,程序人生,职场和发展

前言

测试代码的写法可以归纳为三部分

第一部分: 准备测试数据和定义mock行为

第二部分: 调用真实的函数

第三部分: 调用验证函数进行结果的验证

Junit4

在模块的test路径下编写测试案例。在类中使用@Test注解,就可以告诉Junit这个方法是测试方式。同时使用assert*方法,可以调用Junit进行结果的验证。

@Test
public void test() {
    assertEquals("abc", getActual());
}

Junit常用注解

除了@Test注解,还有以下常见的注解可供使用

android单元测试private变量,软件测试,单元测试,软件测试,功能测试,自动化测试,程序人生,职场和发展

可选使用Junit的Rule简化代码

和Junit的@Before和@After分别作用于每一个单元测试案例的开始和结束类似,@Rule注解提供了同样的能力,但有一个好处就是执行前,执行单元测试和执行后在同一个方法中,包含在同一个上下文中,这能让我们更加灵活的处理单元测试。

使用起来也比较简单:

第一步:实现TestRule接口

public class MethodNameExample implements TestRule {
 
    @Override
 
    public Statement apply(Statement base, Description description) {
 
        //想要在测试方法运行之前做一些事情,就在base.evaluate()之前做
 
        String className = description.getClassName();
 
        String methodName = description.getMethodName();
 
        base.evaluate();  //这其实就是运行测试方法
 
        //想要在测试方法运行之后做一些事情,就在base.evaluate()之后做
 
        System.out.println("Class name: "+className +", method name: "+methodName);
 
        return base;
 
    }
 
}

第二步:在Test类中使用。加上@Rule注解即可

@Rule
public MethodNameExample methodNameExample = new MethodNameExample();
使用Parameterized特性减少重复测试用例(Junit5自带,Junit4需额外引入依赖)

根据不同的输入,待测试函数会有不同的输出结果,那么我们就需要针对每一类的输入,编写一个测试用例,这样才能覆盖待测函数的所有逻辑分支。(写多少个测试用例能够覆盖全所有的逻辑分支可称之为待测函数的圈复杂度).

使用Junit4提供的Parameterized Tests特性,可以帮助我们减少用例编写的代码,使测试类更清晰简单,而且数据可以从CSV文件导入。以下提供一个例子 ( 官方例子见参考资料 [11] ) :

第一步:引入依赖

testImplementation(rootProject.ext.dependencies.jupiter)

第二步:测试类中添加注解

@RunWith(Parameterized.class)
 
public class BioTest {
 
}

第三步:可以定义实例变量,明确输入和输出。比如这里,我们定义了2个变量,一个是预期的输出结果,一个是输入的参数。

private boolean expectedResult;
private String inputTime;

第四步:定义构造函数,在构造函数中对变量赋值

public BioPayProviderTest2(boolean expectedResult, String time) {
    this.expectedResult = expectedResult;
    this.time = time;
}

第五步:定义数据集,使用注解标注,返回一个数组,数组代表的就是Junit4需要提供给构造函数进行实例化的数据集。

@Parameterized.Parameters
public static Collection<Object[]> data() {
    return Arrays.asList(new Object[][]{
        {true, null},
        {false, System.currentTimeMillis() + ":"},
        {true, (System.currentTimeMillis() - 73 * 3600) + ":"}
    });
}

第六步:编写测试用例。对于以下的测试用例,Junit4会使用第五步的数据进行填充,执行3次。

@Test
public void test() throws Exception {
    boolean ret = needShowDialog(time, mockWalletId);
    Assert.assertEquals(expectedResult, ret);
}

Mockito

Mockito是目前使用比较广泛的Mock框架,他可以根据需要mock出虚假的对象,在测试环境中,可以用来替换掉真实的最像,达到两大目的:

  1. 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
  2. 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作

使用Mockito mock对象的某些方法的行为

// 使用mock函数即可模拟一个对象,这个对象的实例变量均为默认值
BioGuidePresenter presenter = Mockito.mock(BioGuidePresenter.class);
 
// 设定这个mock对象被调用某一方法时,应该返回的结果
Mockito.when(presenter.checkIsEnrolled(1)).thenReturn(true);

使用Mockito Spy对象

// 使用spy函数即可模拟一个对象,这个对象的实例变量均为默认值
BioGuidePresenter presenter = Mockito.spy(BioGuidePresenter.class);
 
// 设定这个mock对象被调用某一方法时,应该返回的结果
Mockito.when(presenter.checkIsEnrolled(1)).thenReturn(true);

spy()和mock()的区别在于,未指定mock方法的返回值时,默认返回null,而为指定spy方法的返回值时,默认执行目标方法的逻辑,并返回对应逻辑执行的结果。另外有一个很重要的区别在于,使用spy的情况下,虽然提供了函数的模拟实现,但Mockito框架仍然会调用真实的代码,所以如果真实代码无法在单测下运行,则使用spy模拟会导致测试失败。

使用Mockito验证结果

// 验证mock的database对象,调用setUniqueId方法时的入参是否为12
verify(database).setUniqueId(ArgumentMatchers.eq(12));
 
// 验证mock的database对象的getUniqueId方法是否被调用2次
verify(database, times(2)).getUniqueId();
 
// 验证mock的database对象的getUniqueId方法是否被调用1次
verify(database).getUniqueId();
 
// 也可以使用传统的Junit判断方法判断结果是否符合预期
assertEquals("foo", spy.get(0));

使用Mockito-inline模拟静态方法

Mockito版本升级之后,支持对Static Method做Hook。前提是在build.gradle中引入Mockito-inline。

org.mockito:mockito-inline:${version}

以下是实例被测代码,当我们在测试类中调用doSomethings(),很可能无法通过调整MemoryService的返回值,控制doSomthing2的入参,从而覆盖更多的逻辑分支。此时我们就需要Hook DataEngine甚至是MemoryService,获取我们想要的返回值。

public void doSomethings() {
    DataEngine.getMemoryService().saveCacheObject("key", "abc");
    ...
    String a = DataEngine.getMemoryService().getCacheObject("key");
    doSomething2(a);
}

下面给出使用mockito-inline对静态方法的处理步骤。

一.Hook静态方法。 使用Java7的try-with-resource语法,模拟触发静态方法 (DataEngine .getMemoryService ) 的行为。

需要注意的是:mockService可以是通过Mockito mock出来的,也可以是我们创建的一个真实的MemoryService子类,区别在于,使用Mockito mock的MemoryService我们不需要实现所有的方法,只需要mock我们测试类中可能调用到的方法。

MemoryStoreService mockService = Mockito.mock(MemoryStoreService.class);
try (MockedStatic<DataEngine> service = Mockito.mockStatic(DataEngine.class)) {
    service.when(DataEngine::getMemoryService).thenReturn(mockService);
}

二. 使用更加智能的模拟返回方法。

我们使用较多的是thenReturn()方法,但是在本案例的场景下,我们需要功能更强大的返回方法。因为:处理模拟的入参,

1. MemoryService::saveCacheObject返回值是Void,所以无法使用thenReturn()

2. 我们需要处理入参,针对每一个saveCacheObject的模拟调用,我们都需要真实的将其保存到Map中

final Map<String, Object> pools = new HashMap<>();
 
//当触发了mockService的saveCacheObject方法,就会回调answer(),从而将入参的Key和Value保存到Map中
Mockito.doAnswer(new Answer() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
        pools.put((String) invocation.getArgument(0), invocation.getArgument(1));
        return null;
    }
}).when(mockService).saveCacheObject(Mockito.anyString(), Mockito.any());

当我们使用doAnswer模拟了saveCacheObject,那我们很有可能需要使用同样的策略模拟getCacheObject。就像这样:

Mockito.when(mockService.containsCachedObject(Mockito.anyString()))
    .thenAnswer(invocation -> pools.containsKey(invocation.getArgument(0)));

使用Mockito测试异步代码段

假如需要测试一段异步代码,可以使用标准的异步代码测试步骤进行。举例如下:

public void update(List<Demo> demos) {
    repo.refresh(demos, () -> {
        doSomething();
    });
}

针对上述代码,测试的基本思路是:

步骤一: 模拟一个异步回调函数

//1.判断需要的回调函数的类型,创建ArgumentCaptor
ArgumentCaptor<Repo.OnRefreshListener> captor =
ArgumentCaptor.forClass(Repo.OnRefreshListener.class);
 
//2.主动调用相应函数,触发Mockito框架的执行流进行到回调函数,同时将captor.capture()作为入参传入。
Mockito.verify(repo).refresh(Mockito.anyList(), captor.capture());
 
//3.通过captor.getValue()模拟异步回调函数
Repo.OnRefreshListener mockListener = captor.getValue();

步骤二: 主动调用异步回调接口,从而使执行流进入回调函数中

//主动调用异步函数接口,使得测试执行流进入函数体
mockListener.onResult();

步骤三: 判断是否执行了doSomething()方法,或者执行结果是否符合预期的其他判断方式。

使用Mockito-inline测试静态方法的异步代码段

假如需对以下代码进行单元测试,我们就需要用到mockito-inline.可以看到,RPC请求是通过一个静态方法发出,并且通过异步回调的形式返回结果。

public void demo(String id) {
    RpcService.send(new DemoReq(id), new RpcCallback<DemoResp>() {
        @Override
        public void onFailure(BaseReq call, String msg, String procCd, String procSts, Exception e) {
            if(listener != null){
                listener.onFailure(msg, procCd, procSts);
            }
        }
        @Override
        public void onResponse(BaseReq call, WalletDetailRespMsg response) {
            if(listener != null){
                listener.onSuccess(response);
            }
        }
    });
}

具体写法关键在定义拦截后的行为,invacation保留了调用信息,根据序号获取入参,可以对入参进行判断,之后就可以主动调用回调函数。

try (MockedStatic<RpcService> rpcMock = Mockito.mockStatic(RpcService.class)) {
    //告诉mockito,遇到RpcService.send(参数任意,参数任意)的时候,拦截
    rpcMock.when(() -> RpcManager.sendFromNative(Mockito.any(), Mockito.any()))
        .then(invocation -> {
            //拦截之后,会进入到这里。
            //invocation会保留调用的信息。通过getArgument可以获取入参
            RpcCallback callback1 = invocation.getArgument(1, RpcCallback.class);
            //主动调用callback,可以指定回调入参
            callback1.onResponse(Mockito.mock(BaseReq.class),
            Mockito.mock(WalletDetailRespMsg.class));
            return null;
        });
 
        //主动调用被测方法
        presenter.refreshWalletDetail(testWalletId, callback);
        Mockito.verify(callback).onSuccess(Mockito.any());
}

使用Kotlin封装Mockito-inline单测公用方法

如上,我们若使用java的try-catch-resources会显得代码臃肿,于是我们可以尝试Kotlin简化。 对于try-catch-resources,kotlin中的等价写法是使用use

//mock了Apps.getApp()这个静态方法的返回结果。传入一个高阶函数,易于进行串联调用。
fun getAppMock(action: () -> Any?) {
    Mockito.mockStatic(Apps::class.java).use { appUtilsMock ->
        appUtilsMock.`when`<Void> { Apps.getApp() }.thenReturn(null)
        action()
    }
}

如果我们封装了大量的公用mock代码,那么一段测试代码就长这样:

@Test
fun reduceWithUserRejectTest() {
    val change = HceDefaultChange(true)
    getAppMock {
        isNfcDefaultPaymentMockStatic(true) {
            checkNetMockStatic(true) {
                val actual: PaymentPageState = change.reduce(PaymentPageState())
                Assert.assertTrue(actual.showWaving)
            }
        }
    }
}

是不是和写Flutter或者Compose的UI页面一样啦~

Roboletric

如果不测试Activity页面,则不建议使用Roboletric,一是因为mockito已经能够完成几乎全部的工作,并不需要用到Roboletric,二是用Roboletric影响测试执行速度。

编写可运行的Roboletric单元测试方法

// 首先需要添加RobolectricTestRunner,作为运行Roboletric的启动器
 
@RunWith(RobolectricTestRunner.class)
 
// 其次需要使用Config配置本次单元测试的基础配置。
 
// 1. 如果你的电脑上运行的JAVA版本不是11以上,则需要指定sdk版本为Android 9.0以下
 
// 2. 可以指定shadows。shadows下文会详细解析,这里可配置可不配置,取决于具体场景
 
// 3. qualifiers可以配置机器的尺寸,多语言环境等,可配置可不配置,取决于具体场景。例子中指定了中文环境
 
@Config(sdk = {Build.VERSION_CODES.O_MR1},
 
        shadows = {DemoShadow.class},
 
        qualifiers = "zh"
 
)
public class DemoTest {
}

使用Roboletric模拟Activity

Roboletric的一大特点就是可以模拟Android的context。 我们可以再@Before注解的方法中使用Roboletric创建一个Activity,

@Before
public void initActivity() {
    //Intent可选
    Intent faceIntent = new Intent();
    faceIntent.putExtra(DEMO, uri.toString());
    activity = Robolectric.buildActivity(VerificationBioGuideActivity.class, faceIntent)
            .create().resume().get();
}

Roboletric调用buildActivity即可模拟一个Activity,调用create可以触发onCreate回调,调用resume可以触发onResume回调,最后调动get就可以拿到这个activity对象。拿到activity的对象之后,我们就可以通过activity进行一些操作了。例如,获取View的控件,获取字符串等。

// 获取View控件
TitleBar titleBar = (TitleBar) activity.findViewById(R.id.title_bar);
 
// 获取字符串
activity.getString(R.string.verification_bio_pay_title_finger_success_tips)

可以使用Roboletric模拟出来的activity作为context,如果只需要用到applicaitonContext,可以使用

RuntimeEnvironment.getApplication()

Roboletric的杀手锏——Shadows

Robolectric的本质是在Java运行环境下,采用Shadow的方式对Android中的组件进行模拟测试,从而实现Android单元测试。

Shadows的作用就是使用自定义的方法和类替换原先业务的方法和类,原理就是使用字节码修改技术进行动态的修改。例如,业务中A.class原先要调用B.class的C()函数,我们使用Shadows,并定义一个函数签名一样的函数D()将其作用于C()函数上。当触发A.class的调用后,程序执行流程会进入D()函数中。

自定义Shadows简介

第一步: 使用@Implements类定义需要被替换的目标类。Shadows类本身也是一个普通的Java类,只不过多了@Implements注解,所以他也可以实现接口,继承等功能。

// 表明这个Shadow期望模拟ActualClass类的行为
@Implements(ActualClass.class)
@RequiresApi(api = Build.VERSION_CODES.N)
public class ActualClassShadow {
 
}

第二步: 与目标函数使用相同的函数签名,并且增加@Implementation注解。可以只替换目标类中的部分函数,而其他函数仍然遵循目标类的逻辑。

//表明需要模拟ActualClass中的containsKey方法
@Implementation
public boolean containsKey(String key) {
}

第三步(可选):__constructor__可以替换构造函数

public void __constructor__(int x, int y) {
    realPoint.x = x;
    realPoint.y = y;
}

第四步(可选):使用@RealObject可以获取目标对象

@RealObject private Point realPoint;

第五步:定义好Shadows之后,我们需要再测试类的Config注解中进行注册。

@Config(shadows = {ActualClassShadow.class})

使用Chat-GPT生成单元测试案例

紧跟时事!!

我们可以使用Chat-GPT快速生成测试案例,分为以下几步:

第一步:让Chat-GPT知晓该函数的意图

第二步:告诉Chat-GPT单元测试的目标

Optional:可以指定Chat-GPT使用Junit4的一些测试特性简化单测案例

序号    Chat-GPT    Input
1        java (输入待测代码)what's the author's intentions
2    The author's intention with this Java code is to provide a methodhexStringToByteArray that can convert a hexadecimal string to abyte array. The method takes a single argument data, which is thehexadecimal string to be converted. The code first checkswhether the input string is valid and has an even number ofcharacters, since each pair of characters in the string representsa single byte. If the input string is valid, the code creates a new byte arrayresult with a length equal to half the length of the input string(since each pair of characters represents one byte). It then loopsover the input string in pairs, converting each pair of charactersto a byte and storing it in the result array. If the input string is not valid, the code logs an error message andreturns null. Overall, the intention of this method is to provide a simple andreliable way to convert hexadecimal strings to byte arrays inJava.    
3        A good unit test suite should aim to:- Test the function's behavior for a wide range of possible inputs- Test edge cases that the author may not have foreseen- Take advantage of the features of Parameterized to make thetests easy to write and maintain- Be easy to read and understand, with clean code anddescriptive names- Be deterministic, so that the tests always pass or fail in the same wayJunit has many convenient features that make it easy to write and maintain unit tests. We'll use them to write unit tests for the function above.For this particular function, we'll want our unit tests to handle the following diverse scenarios
4    Since the code provided is in Java, we'll write our unit tests inJava using the JUnit framework. Here are some example unittests we might write for the hexStringToByteArray function: @RunWith(Parameterized.class)public class DesUtilsHexStringToByteArrayTest {    @Parameterized.Parameters    public static Collection<Object[]> data() {        return Arrays.asList(new Object[][]{                // Test cases where input is valid                {"00", new byte[]{0x00}},                {"7f", new byte[]{0x7f}},                {"deadbeef", new byte[]{(byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef}},                {"0102030405060708090a0b0c0d0e0f", new byte[]{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}},                // Test cases where input is invalid                {"", null},                {"0", null},                {"1", null},                {"a", null},                {"g", null},                {"gg", null},                {"000", null},                {"001", null},                {"00g", null},                {"0x00", null},                {"deadbeef0", null}        });    }     private String input;    private byte[] expected;     public DesUtilsHexStringToByteArrayTest(String input, byte[] expected) {        this.input = input;        this.expected = expected;    }     @Test    public void testHexStringToByteArray() {        byte[] actual = hexStringToByteArray(input);        assertArrayEquals(expected, actual);    }} In this example, we're using JUnit's Parameterized runner togenerate multiple test cases from a single test method. The datamethod returns a collection of test cases, each represented as anarray of input parameters and expected output. We have included test cases for a wide range of possible inputs,including valid input strings with different lengths, as well asinvalid input strings that don't have an even number ofcharacters, contain non-hexadecimal characters, or exceed themaximum length that can be converted to a byte array. We've used descriptive names for our test class, test method,and test parameters to make the tests easy to read andunderstand. Finally, our tests are deterministic, since they will always pass or fail in the same way.

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

android单元测试private变量,软件测试,单元测试,软件测试,功能测试,自动化测试,程序人生,职场和发展

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!  

android单元测试private变量,软件测试,单元测试,软件测试,功能测试,自动化测试,程序人生,职场和发展文章来源地址https://www.toymoban.com/news/detail-762873.html

到了这里,关于Android下单元测试实践——测试框架简介的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • Android 单元测试之PowerMock

    class PowerMockClassTest { @Test fun isFileExists() { // Mock 一个 File对象 val file = PowerMockito.mock(File::class.java) // 创建当前类 val powerMockitoClass = PowerMockClass() // 当Mock对象被调用了 exists() 方法,则返回False PowerMockito. when (file.exists()).thenReturn(false) // 进行断言 assertFalse(file.exists()) } } 对于这种

    2024年04月23日
    浏览(30)
  • Android 自动化单元测试

    2024年02月13日
    浏览(32)
  • Android 单元测试之 Mockk

    relaxed : 是否对其代码进行依赖,默认为否,这个参数比较关键,后续会更细的讲解一下 moreInterfaces : 让这个mock出来的对象实现这些声明的接口 relaxUnitFun :和 relaxed 差不多,但是只针对于 返回值是Unit 的方法, 后续会讲解一下 block : 该语句块表示你在创建完 mock 对象后

    2024年02月02日
    浏览(19)
  • Android Studio系列-Activity单元测试,字节Android高级岗

    新建Activity单元测试类 =============== package com.devilwwj.unittestdemo; import android.content.Intent; import android.test.ActivityUnitTestCase; import android.test.suitebuilder.annotation.MediumTest; import android.widget.Button; /** Created by wwj_748 on 2016/2/22.17.12 */ public class LoginActivityTest extends ActivityUnitTestCase { private Inten

    2024年04月25日
    浏览(51)
  • 猫耳 Android 播放框架开发实践

    猫耳FM是中国最大的 95 后声音内容分享平台,是B站重要平台之一,深度合作国内顶级声优工作室,打造了数百部精品广播剧,全站播放总量超过百亿次。 MEPlayer 是猫耳 Android 技术团队研发的一款适用于音视频、直播、特效播放等多种场景的跨进程播放框架。目前支持: 音视

    2024年02月06日
    浏览(32)
  • 如何优雅地单元测试 Kotlin/Java 中的 private 方法?

    翻译自 https://medium.com/mindorks/how-to-unit-test-private-methods-in-java-and-kotlin-d3cae49dccd ❓如何单元测试 Kotlin/Java 中的 private 方法❓ 首先,开发者应该测试代码里的 private 私有方法吗? 直接信任这些私有方法,测试到调用它们的公开方法感觉就够了吧。 对于这个争论,每个开发者都会

    2024年02月06日
    浏览(30)
  • Android单元测试系列(3)-Mock之Mockito

    目录 一、官网 二、Demo示例 1. 目录结构 2. 被测试的类 3. 测试类 三、Mockito方法说明 1. mock对象创建 2. Mockito框架中的常见方法说明 2.1 常见的打桩方法 2.2 常见的验证行为 2.3 其他方法  3. Mockito的局限性 Mockito: https://github.com/mockito/mockito Mockito (Mockito 4.4.0 API) 为什么要用mock:

    2023年04月15日
    浏览(26)
  • Android单元测试系列(3)-Mock之PowerMock

    目录 一、官网 二、Demo示例  三、PowerMock常用的测试方法 1. Private 1.1 私有变量 1.2 私有方法 2. Final 3. Static Android单元测试系列(3)-Mock之Mockito_Chris_166的博客-CSDN博客 Android单元测试系列(1)-开篇_Chris_166的博客-CSDN博客 这两篇中已经分别介绍过Mockito的使用和局限性,本篇将介绍P

    2023年04月08日
    浏览(25)
  • junit单元测试mock私有private方法和静态static方法

    我们知道org.mockito.Mockito功能有限,不能mock 私有private、受保护的protected方法 org.powermock.api.mockito.PowerMockito更强大,支持对private和protected和static方法的mock 别忘记,首先要引入maven依赖 有如下私有方法需要mock 这时候可以利用PowerMockito的spy方法mock出方法所在的对象,然后利用

    2024年02月12日
    浏览(28)
  • Android 单元测试只看这一篇就够了

    目录 单元测试的目的以及测试内容 本地测试 1. 添加依赖,google官方推荐: 2. 单元测试代码存储位置: 3. 创建测试类: 4. 运行测试用例: 5. 通过模拟框架模拟依赖,隔离依赖: 仪器化测试 配置: 例子: 常用单元测试开源库 1. Mocktio 2. powermock 3. Robolectric 实践经验 1. 代码中用到了

    2024年02月03日
    浏览(41)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包