单元测试的最佳实践

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

1、背景

本文1-2章是单测的介绍,如果需要直接看整合教程,直接跳至第3章看干货;

在复杂系统的开发中,我们经常需要做单元测试。但又由于单元测试没有形成标准,而经常遇到这样的问题:

  • 直接写一个main函数对需要的方法进行测试
  • 单元测试过于复杂,有时候又懒得做单元测试
  • 开发时需要将项目启动起来对功能进行验证,但同时又没有一个独立的环境;
  • 对功能验证时,Debug10秒钟、启动项目5分钟,修改后又要重新启动项目;
  • 测试时外部依赖不可用,影响正常开发;

而不做单元测试,将又会带来:

  • 业务与逻辑愈发复杂,不敢随便乱改动代码,久而久之就在屎山上面堆屎;
  • 一段时间不看逻辑,就会忘记这段代码应该怎么跑
  • 改动别人的代码后,不知道是否会引起其他连锁反应
  • 需求复杂,需要边测试验证边开发

针对以上问题,本文将介绍基于集成Mockito + PowerMock + H2 + EmbededRedis 的单元测试实践方案,整套单元测试环境将完全脱离Spring框架进行,使得功能验证更加纯粹简单。

2、单元测试介绍

Java的单元测试就是测试各个独立方法的功能是否符合预期以及边界值限定的情况。单元测试不只是为了验证你当前写的代码是否存在问题,更为重要的是他可以很大程度保障日后因业务变更,开发成员变更,修复BUG或者重构引起的代码变更而导致(或新增)的风险

2.1、单元测试的AIR原则

A:Automatic(自动化)

即单元测试具备可自动化运行的能力,测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。如: 集成CI能力,在每一次commit过后,在pipeline中进行各项单元测试集合的自动化运行(不准使用 System.out 来进行人肉验证,必须使用 assert 来验证),并给出自动化单元测试运行的结果(pass/fail);

I:Independent(独立性)

为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序;单元测试也不依赖任何外部的系统或组件(如外部系统、外部Mysql、外部密钥等),而是自身具备独立可测试的能力,方便于任何第三方拿到单测之后都可以直接运行并验证功能;

R:Repeatable(可重复)

单元测试应该是可重复的,每次运行的运行都应该不应该受到外部系统或者外部数据影响。一旦数据和可用性问题,结果就会像幽灵一样时好时坏;也不能因为这次创建了数据,下次就因为主键约束不能再创建。

2.2、使用到的工具与开源组件

  • 单元测试框架:Junit4/5
  • 单元测试工具:Mockito + PowerMock
  • 内存数据库:H2 / EmbededMysql / EmbededRedis
  • 内存中间件:TestContainer

单元测试的最佳实践

2.3、单元测试能干啥

  • 有更快的 【开发-验证】循环,无需等待依赖实现,外部实现,无需特定环境,帮助开发应对业务的快速变化;
  • 节省调试时间(越复杂的功能,潜在收益越大,简单CRUD可能看不出什么);
  • 便于多人协作与项目交接;
  • 极大增强了变革的信心,增加重构自信;
  • 单元测试与程序设计互相赋能;
  • 提升项目效率,快速、高质量发布;

2.4、单元测试常见的问题

  • 自身依赖:单测需要依赖自身服务和自身环境,必须要完整启动项目才能做测试;
  • 幽灵测试:一旦有数据和可用性问题,结果就像幽灵一样时好时坏;
  • 外部依赖:有些单测写成了集成测试,依赖外部服务,或者单个测试庞大而臃肿
  • 效率低下:本地启动不起来,提交到远程又时好时坏,跑一次还10分钟
  • 场景不全:多数只是关注了正常场景,边界值和异常指标没有覆盖到
  • 指标不足:测试粒度太大,场景不全,没有关注到覆盖率,异常率和逻辑覆盖率

3、单元测试的实践介绍

单元测试实际上建议单独一个项目模块(实在不行则在项目顶级依赖如start项目下做),否则每个模块都要重新做一次单独单测环境;

简单来说,本章节将会脱离Spring环境,转而使用完全的Mockito环境进行单元测试。

即:使用内存数据库H2作为数据源,使用Mockito对目标对象进行打桩与验证、使用PowerMock对静态方法、私有方法进行模拟与打桩。

有什么好处吗? 完全脱离Spring环境之后,使得单元测试更加纯粹,项目启动速度特别快;

有什么坏处吗?Spring的特性就无法使用了,比如Autowire一个List和Autowire一个Map等;Mybatis等数据源的注入需要手工注入了;

3.1、引入开源组件

具体版本以最新为准

3.1.1、Mockito

用于脱离Sping环境进行单元测试,对数据进行打桩,对外部数据源、外部服务进行模拟

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>3.9.0</version>
            <scope>test</scope>
        </dependency>

3.1.2、PoweMock

用于mock静态方法、私有方法等,是mockito的增强使用


        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4</artifactId>
            <version>2.0.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito2</artifactId>
            <version>2.0.2</version>
            <scope>test</scope>
        </dependency>

3.1.3、H2内存数据库

用于单侧时启动一个本地的内存数据库进行数据源准备与数据库数据的初始化

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>

3.1.4、EmbededRedis (可选)

若单测时需要使用到redis的能力,可以使用embededRedis

        <dependency>
            <groupId>com.github.kstyrc</groupId>
            <artifactId>embeded-redis</artifactId>
            <version>0.6</version>
        </dependency>

实际上,实际上,可以使用testcontainer拉取一个镜像并运行一个临时实例来代替(但个人感觉这样有点慢)

3.2、单元测试的准备工作

完成了如上的依赖引入之后,我们便有了一套真正意义上的单元测试工具了。

本节将阐明如何脱离Spring进行单元测试前的准备:

  • 使用原生mybatis进行数据操作
  • 准备前期的打桩数据构造器
  • 编写单元测试基类

3.2.1、增加适用于H2数据库连接的mybatis配置

因为单元测试脱离了Spring,因此我们要引入新的mybatis.conf,因此我们在test目录下,添加一个适用于H2数据库连接的mabatis-test.conf

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <settings>
        <setting name="jdbcTypeForNull" value="NULL"/>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLER">
                <property name="url" value="jdbc:h2:mem:test"/>
                <property name="driver" value="org.h2.Driver"/>
                <property name="username" value="test"/>
                <property name="password" value="test"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <!--这里填入你需要引入的MAPPER.XML-->
    </mappers>
</configuration>

3.2.2、增加本地文件读取工具

先增加一个可以读取本地文件的工具ReadFileUtil,专门用于读取ddl.sql与预置sql数据。(这里偷懒,直接用了Spring的StreamUtils和Fastjson的序列化能力)

package com.teamer.teapot.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.nio.charset.Charset;

/**
 * @author tanzj
 * @date 2022/9/17
 */
public class ReadFileUtil {

    /**
     * 用于从指定路径中读取资源,并转换为特定对象
     *
     * @param path          路径
     * @param typeReference 类型
     * @param <T>           类型
     * @return 指定类型的对象
     */
    public static <T> T readJsonFromResource(String path, TypeReference<T> typeReference) {

        try {
            String jsonString = StreamUtils.copyToString(
                new ClassPathResource(path).getInputStream(), Charset.defaultCharset()
            );
            return JSON.parseObject(jsonString, typeReference);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String readStringFromResource(String fileName) throws IOException {
        return StreamUtils.copyToString(new ClassPathResource(fileName).getInputStream(), Charset.defaultCharset());
    }

}

3.2.3、搭建H2连接工具

编写一个H2DbSupportFactory工具,此工具用于单元测试时Mapper对象的导入

package com.teamer.teapot.util;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.testcontainers.shaded.org.apache.commons.lang.StringUtils;

import java.io.IOException;
import java.io.Reader;
import java.sql.SQLException;
import java.util.Arrays;


public class H2DbSupportFactory {

    public static SqlSessionFactory sqlSessionFactory;

    public static SqlSession sqlSession;

    public boolean dataInited = false;

    static {
        //测试用的mybatis-test文件
        try(Reader reader = Resources.getResourceAsReader("mybatis-test.xml")) {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static H2DbSupportFactory getInstance(String[] ddlPath, String[] initDataPath) throws IOException, SQLException {

        //获取数据库建表语句脚本
        StringBuilder ddlSqlStrBuilder = new StringBuilder();
        for (String eachDdlSqlPath: ddlPath) {
            String currentSql = ReadFileUtil.readStringFromResource(eachDdlSqlPath);
            ddlSqlStrBuilder.append(currentSql);
            if (!currentSql.endsWith(";")) {
                ddlSqlStrBuilder.append(";");
            }
        }

        //获取初始化数据
        StringBuilder initDataPathStringBuilder = new StringBuilder();
        if (!Arrays.stream(initDataPath).allMatch(StringUtils::isEmpty)) {
            for (String eachInitDataPath : initDataPath) {
                String currentSql = ReadFileUtil.readStringFromResource(eachInitDataPath);
                initDataPathStringBuilder.append(currentSql);
                if (!currentSql.endsWith(";")) {
                    initDataPathStringBuilder.append(";");
                }
            }
        }

        String ddlSql = ddlSqlStrBuilder.toString();
        String initDataSql = initDataPathStringBuilder.toString();

        H2DbSupportFactory factory = new H2DbSupportFactory();
        factory.setSqlSession(getSqlSessionFactory().openSession());
        if (factory.getSqlSession() == null) {
            sqlSession = getSqlSessionFactory().openSession();
        }

        if (StringUtils.isNotBlank(ddlSql)) {
            //h2每次只能创建一张表
            String[] ddlSqlList = ddlSql.split(";;");
            for (String eachDdlSql : ddlSqlList) {
                sqlSession.getConnection().createStatement().execute(eachDdlSql);
            }
        }

        if (StringUtils.isNotBlank(initDataSql) || !factory.dataInited) {
            sqlSession.getConnection().createStatement().execute(initDataSql);
            factory.dataInited = true;
        }
        sqlSession.commit();;
        return factory;
    }


    public SqlSession getSqlSession() {
        return sqlSession;
    }

    public boolean isDataInited() {
        return dataInited;
    }

    public H2DbSupportFactory setDataInited(boolean dataInited) {
        this.dataInited = dataInited;
        return this;
    }

}

3.2.4、编写单元测试基类

编写单元测试基类,让所有基于此方案的单元测试直接继承这个基类,从而省去每次编写单测的前置工作。


@RunWith(PowerMockRunner.class)
public abstract class BaseMockitoTest {

    protected static H2DbSupportFactory h2DbSupportFactory;
    
    /**
     * 用于为target注入对象,目前用于Mapper层的手工注入
     * @param target 目标业务对象
     * @param field 字段
     * @param dependency 依赖的对象
     * @throws NoSuchFieldException e
     * @throws IllegalAccessException e
     */
    public void setter(Object target, String field, Object dependency) throws NoSuchFieldException, IllegalAccessException {
        Field targetField = target.getClass().getField(field);
        if (!targetField.isAccessible()) {
            targetField.setAccessible(true);
        }
        targetField.set(target, dependency);
    }

    /**
     * 子类集成时,需要在方法增加@Before注解,用于提前打桩
     * @see org.junit.Before
     * @throws Exception e
     */
    public abstract void before() throws Exception;

    /**
     * 子类集成时,需要在方法增加@BeforeClass注解,用于提前静态类初始化能力的集成
     * @see org.junit.BeforeClass
     */
    public static void init() {

    };


}

3.2.5、增加EmbededRedis的启动器

若是单测需要依赖redis的能力,则需要提前配置好redis的基础配置。案例使用jedis作为redis-client,若使用其他工具,配置也大同小异。


/**
 * @author tanzj
 * @date 2022/9/19
 */
public class EmbeddedRedisHolder {

    private static RedisServer redisServer;
    
    private static Jedis jedis;
    
    private static JedisPool jedisPool;

    /**
     * 启动器,需要在@BeforeClass中调用初始化
     */
    public static void startUp() {
        redisServer = RedisServer.builder()
            .port(63792)
            .setting("maxmemory 64m")
            .build();
        redisServer.start();

        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(200);
        jedisPoolConfig.setMaxWaitMillis(2000);
        jedisPoolConfig.setMaxIdle(8);
        jedisPoolConfig.setMinIdle(0);
        jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 63792, 10000);
    }


    /**
     * 进程销毁,需要在@AfterClass中调用销毁
     */
    public static void end() {
        redisServer.stop();
    }
    
    public static Jedis getJedis() {
        if (jedis != null) {
            return jedis;
        }
        if (jedisPool == null) {
            initJedisPool();
        }
        jedis = jedisPool.getResource();
        return jedis;
    }

    private static JedisPool initJedisPool() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(200);
        jedisPoolConfig.setMaxWaitMillis(2000);
        jedisPoolConfig.setMaxIdle(8);
        jedisPoolConfig.setMinIdle(0);
        jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 63792, 10000);
        return jedisPool;
    }


    public static JedisPool getJedisPool() {
        return jedisPool;
    }
}

3.3、编写业务单元测试

开始编写业务单元测试。我们先模拟一个新增订单的一个业务逻辑。

我们需要对Controller层是简单的路由转发,我们在开发中只需要对OrderServicecreateOrder方法进行业务的单元测试,具体的代码层级结构如下:

  • OrdereController:业务入口
  • OrderService:是业务逻辑层,主要提供了创建单据与查询单据详情的方法,Service层聚合了OrderInfoManager(单据通用逻辑层)与OrderItemDetailManager(单据明细通用逻辑层)
  • OrderInfoManager/OrderItemDetailManager:通用逻辑层,包装了DAO与单据特性的通用能力
  • DAO层:数据访问层
  • 各模块的注入使用了@Autowire注入

单元测试的最佳实践

3.3.1、新增业务单元测试

根据单元测试规范,我们需要在test目录下新建一个与被测类相同路径的单元测试方法,并且以Test结尾,如我们需要测试的方法是:com.teamer.teapot.order.service.impl.OrderServiceImpl,那么此时我们就需要在test目录下新建com.teamer.teapot.order.service.OrderServiceImplTest的单元测试方法:


/**
 * @author tanzj
 * @date 2022/9/21
 */
public class OrderServiceTest extends BaseMockitoTest {
    
    
    /**
     * 子类集成时,需要在方法增加@Before注解,用于提前打桩
     *
     * @throws Exception e
     * @see Before
     */
    @Before
    @Override
    public void before() throws Exception {
        
    }
}

3.3.2、使用@InjectMock注入依赖项

如3.3的大图所示,我们知道OrderService是依赖了OrderInfoManager 与 OrderItemDetailManager 的两个bean,因此在单元测试中,我们需要使用InjectMocks将这两个依赖也定义出来。

@InjectMocks可以将所有使用@InjectMocks, @Spy,@Mock 修饰的字段,全部注入到当前修饰的字段中,有点类似Spring的注入。


public class OrderServiceTest extends BaseMockitoTest {

    /**
     * <br/> 使用InjectMocks定义一个对象时,这个对象会自动注入所有使用@InjectMocks与@Spy,@Mock 注解的字段
     * <br/> 这里使用spy构造是因为我们确实要走他的创建方法
     */
    @InjectMocks
    private OrderService orderService = Mockito.spy(new OrderServiceImpl());

    /**
     * orderService 依赖了orderInfoManager,我们这里同样要注入MockOrderInfoManager
     */
    @InjectMocks
    private OrderInfoManager orderInfoManager = Mockito.spy(new OrderInfoManagerImpl());

    /**
     * 同理
     */
    @InjectMocks
    private OrderItemDetailManager orderItemDetailManager = Mockito.spy(new OrderItemDetailManagerImpl());

    /**
     * 子类集成时,需要在方法增加@Before注解,用于提前打桩
     *
     * @throws Exception e
     * @see Before
     */
    @Before
    @Override
    public void before() throws Exception {

    }
}

此时如果我们任意启动一个单元测试,通过debug,便会发现所有的对象都被注入成功了

单元测试的最佳实践

3.3.3、对value对象进行打桩 

Mockito不推荐我们对value对象进行注入模拟,因此针对于DAO对象,我们直接使用打桩返回的形式进行测试,在单元测试类中注入两个DAO

单元测试的最佳实践

在before方法加上@Before注解,并对这两个Bean进行提前打桩。

单元测试的最佳实践

对mock的对象,打桩格式是:

when(mockBean.doSomething(any())).thenReturn(requireObject);

对spy对象,打桩格式是

doReturn(requireObject).when(spyBean).doSomething(any())

注意:这里的Mock更可以用在第三方系统或者接口、服务尚未准备好的情况下,我们去提前在单元测试中模拟返回(不仅如此,根据AIR原则,我们也必须对第三方接口和服务进行Mock)

3.3.4、使用到了Mysql

基于3.3.3的前提下,我们可以完成大部分的单元测试。但在很多时候,只有真正走到了SQL层面,才能验证你的sql是否准确,字段约束是否齐全,这个时候就需要引入H2内存数据库进行单元测试了。

因此我们在准备好3.2.3的DB连接与初始化工具之后,便可以准备db的初始化了。

我们为单元测试增加init()方法,并为其加上@BeforeClass注解,提前准备好对应表的ddl语句和事先准备好的的数据初始化sql脚本

单元测试的最佳实践

在mybatis-test.conf中注册这两个DAO

单元测试的最佳实践

然后在before()方法中,对DAO进行初始化(注意,如果使用这种方法,需要取消3.3.3中,对dao的@Mock注解)

单元测试的最佳实践

 这样便完成了DAO对象的注入。

此时,我们需要调用基类的setter()方法,利用反射对业务对象中的DAO进行赋值(这里笔者没有找到更好的办法对dao进行注入,如果有更好的办法请一定不吝赐教)

单元测试的最佳实践

完成此步骤之后,单元测试中便完成了数据源与DAO的注入配置。值得注意的是,H2的语法与Mysql有些许不同,比如H2便不支持json操作。若设计到json的字段,请在ddl脚本将其设置为text字段。

3.3.5、使用到了Redis

若被测方法使用到了redis的特性,我们可以使用3.2.5中提前准备好的embededRedis启动器进行redis测试。

我们依然在beforeClass中先启动redis

单元测试的最佳实践

 新建一个方法:destroy,并为此方法新增注解@AfterClass,在此方法中调用redisEnd

 

此时便完成了embededRedis的启动了。而后在业务单元测试方法中,对jedisPool进行注入

单元测试的最佳实践

这样在你使用到redis的位置,用此jedisPool来拉取jedisClient时便可以拿到我们预先配置的bean了

 3.4、编写单元测试

而后,我们针对OrderService每一个业务方法,都进行单元测试,并基于单元测试的标准对其边界值进行测试。并对其结果进行断言(不对结果进行断言的单元测试都是耍流氓)

如:

单元测试的最佳实践

 实际上,参考Mockito官方的写法,我们建议单元测试的方法命名为:

test + ${method} + "_success"

test + ${method} + "${用下划线隔开的场景}"

因为这样,在跑单元测试的边界值测试时,才更能一幕了然的看到所有被测试的场景文章来源地址https://www.toymoban.com/news/detail-441783.html

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

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

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

相关文章

  • 吃透单元测试:Spock单元测试框架的应用与实践

    一,单元测试 单元测试是对软件基本组成单元进行的测试,如函数或一个类的方法。程序是由函数组成的,每个函数都要健壮,这样才能保证程序的整体质量。单元测试是对软件未来的一项必不可少的投资。”具体来说,单元测试有哪些收益呢? 它是最容易保证代码覆盖率

    2024年02月09日
    浏览(50)
  • 测试自动化的 10 个最佳实践

    *以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/qavI7z8IAy8qaiQvuQgURQ 虽然大家都知道坚果是非常健康和有营养的,但是,当你尝试吃它的时候,我猜测过程都不会很顺利。 现实就是那么相似,我们都知道测试自动化对软件

    2024年02月02日
    浏览(48)
  • ChatGPT 最佳实践指南之:系统地测试变化

    Test changes systematically 系统地测试变化 Improving performance is easier if you can measure it. In some cases a modification to a prompt will achieve better performance on a few isolated examples but lead to worse overall performance on a more representative set of examples. Therefore to be sure that a change is net positive to performance it may be

    2024年02月16日
    浏览(36)
  • 前端单元测试与自动化测试实践

    在前端开发中,单元测试和自动化测试是保证代码质量和稳定性的重要手段。通过编写和执行测试用例,可以及早发现代码中的问题,并确保代码在不同环境下的正确运行。本文将介绍前端单元测试和自动化测试的实践,并通过一个示例说明其重要性和具体操作。 前端单元测

    2024年02月12日
    浏览(49)
  • Android下单元测试实践——测试框架简介

    测试代码的写法可以归纳为三部分 第一部分: 准备测试数据和定义mock行为 第二部分: 调用真实的函数 第三部分: 调用验证函数进行结果的验证 在模块的test路径下编写测试案例。在类中使用@Test注解,就可以告诉Junit这个方法是测试方式。同时使用assert*方法,可以调用J

    2024年02月04日
    浏览(39)
  • 单元测试的实践与思考

    之前一直有一个想法:将测试过程的每个重要环节都进行拆解,然后详细说明这个环节重点要做的事情,为什么要做这些事,以及注意事项。在星球群里和几位同学聊到了这个事情,有同学提议可否将单元测试环节加进来,斟酌一番,觉得还是很有必要的,就有了今天的这篇

    2024年02月05日
    浏览(41)
  • 单元测试规范和实践

    软件测试按照阶段划分,一般可以分为如下四类: 单元测试 单元测试是对软件组成单元进行测试。其目的是检验软件基本组成单位的正确性。测试的对象是软件设计的最小单位:函数。常常由开发人员依据详细的设计文档进行测试,包含接口测试、局部数据结构测试、路径

    2024年02月01日
    浏览(52)
  • 单元测试方法-cmockery实践

    目录 单元测试概念 引子 定义 内容 方法 单元测试模型 测试模型构建 单元测试工具简介 Cmockery使用介绍 简介 使用 VPBX实践 UT框架搭建 目录 编译: 实例demo 例1: 例2: 例3: 例4: 例5: 例6: 例7: 遗留问题 一种观点:“实际工作中,写好程序后对程序功能的调试就是一种

    2023年04月11日
    浏览(39)
  • Airtest图像识别测试工具原理解读&最佳实践

    Airtest是一个跨平台的、基于图像识别的UI自动化测试框架,适用于游戏和App,支持平台有Windows、Android和iOS。Airtest框架基于一种图形脚本语言Sikuli,引用该框架后,不再需要一行行的写代码,通过截取按钮或输入框的图片,用图片组成测试场景,这种方式学习成本低,简单易

    2024年02月09日
    浏览(37)
  • APP流水线测试领域探索与最佳实践

    APP端UI自动化因其特殊性(需连接测试机)一般都在本地执行,这种执行方式的局限性有以下弊端: 时效性低:研发每次打包后都需要通知测试,测试再去打包平台取包,存在时间差 研发自测或产品验收无法使用自动化脚本:研发自测及产品验收时如果想用自动化脚本需要搭

    2024年02月16日
    浏览(50)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包