怎样才算完整的接口自动化测试案例?

news2024/10/2 14:21:22

代码 VS 低(零)代码平台

你在公司里开展自动化测试,是使用纯代码的方式还是利用已有的低代码或者零代码平台?本人的观点,一直很清晰,自动化测试,最佳的方案就是走纯代码。为啥?一定会有人跳出来反驳我,说“我们公司的低代码平台/零代码平台就很好用啊,很方便啊”。没错,好用的一定会有,但是大概率没有深入去实现一个自动化测试用例,能把一个自动化测试用例写清楚,并且具备高可维护性的低代码/零代码平台不是没有,而是需要很多年去打磨,才能做得出来。有公司愿意去付出这样的代价吗?投入产出比未免有点低。纯代码的方式去实现自动化测试其实更合理一些。也许有人会反驳我,觉得低代码/零代码平台能让更多编码能力不行的测试人员上手自动化测试。我倒觉得这是一种退步,技术方向的退步。测试工程师作为IT从业人员,而且是技术类的,不会写代码首先就是不合理的,而且降低行业要求,其实等于降低行业门槛,从而也毁了这个行业,从这个角度,我更是反对低代码或者零代码平台。当然,这是我的个人观点,也代表了个人的坚持,不代表观点一定正确。

 

接口测试自动化案例的要素

一个好的接口自动化测试应该包含哪些要素?总结一下,基本需要这些:

  • 环境

  • 前置条件(比如数据准备、登录信息等)

  • 测试数据

  • 接口定义

  • 操作步骤

  • 断言

  • 清理

所有语言都是苍白的,咱们来具体看个例子。

目标接口

我们这里不举登录接口的例子,那都臭了街了,网上说的太多,这里举一个具有典型性的例子:修改订单接口————modify

先看接口的定义:

路径:/order/{orderNbr}/modify

请求方式:GET

URL参数:address="新地址"

Header:Access-Token: Token串

返回:

1
2
3
4
{
  "code": 1000,
  "message": "success"
}

orderNbr为订单编号

这个接口需要先有登录后的Token,需要存在一份已有订单,才可以成功修改,并且这样的修改,影响了数据库的数据(写接口的特点)。

编写接口测试代码

我们这里用Rust为例来写测试代码。

代码文件组织

我们用一个文件对应一个接口,这样有利于接口测试的管理。这里我们创建一个文件叫modify.rs,同modify接口名称保持一致。

至于其他的,比如数据库操作的通用方法,cache操作的通用方法等等,可以另建mod来整理。

准备第三方依赖

根据接口测试的特点,我们需要准备好我们所需要用到的第三方库,比如请求库数据库连接库等。看如下清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 请求库
reqwest = { version = "0.11", features = ["json"] } 
# 异步库
tokio = { version = "1", features = ["full"] } 
# 序列化库
serde_json = "1.0.88"
serde_derive = "1.0.147"
serde = "1.0.147"
# 测试参数化库
rstest = "0.15.0"
# url解析库
url = "2.3.1"
# redis库
redis = "0.22.1"
# mysql库
mysql = "23.0.1"
# 日志库
tracing = "0.1.37"
tracing-test = "0.2.3"

定义接口

我们首先在modify.rs中定义好这个接口的请求结构和响应结构,当然这个接口因为是GET请求,没有请求的body,所以请求结构这里就不定义了:

1
2
3
4
5
6
7
// modify.rs
// Modify接口定义返回结构体
#[derive(Deserialize, Serialize, Debug)]
pub struct ModifyResponse {
    pub code: u32,
    pub message: String,
}

然后定义接口调用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// modify.rs

// 定义Modify接口调用函数
pub async fn modify(base_url: &str, token: &str, order_number: &str, address: &str) -> Result<ModifyResponse, Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();

    // address字符串因为要放在URL中,所以需要进行URL编码
    let address = url::form_urlencoded::byte_serialize(address.as_bytes()).collect::<String>();

    // 根据环境拼装接口地址
    let url = format!("{}/order/{}/modify?address={}", base_url, order_number, address);

    // 发送请求
    let resp = client.get(&url).header("Access-Token", token).send().await;
    info!("modify resp: {:?}", resp);

    
    // 处理返回结果
    return match resp {
        Ok(resp) => {
            let resp = resp.json::<ModifyResponse>().await;
            match resp {
                Ok(resp) => Ok(resp),
                Err(err) => Err(Box::new(err)),
            }
        }
        Err(err) => Err(Box::new(err)),
    };
}

注意一定要把请求的url拆成两部分,一部分是主机地址(base_url),一部分是路径。这样针对不同的环境,只需要更换主机地址就可以了。这样的调用,可以匹配不同的环境。

测试案例

接下来就是写针对这个接口的测试案例了。

首先我们要考虑这个接口的依赖,这个接口必须在header中传递Access-Token,来传递登录信息。我们可以有两种方式处理,一种,是在调用这个接口之前,先调用登录接口来获取这个Token,并把这个Token加入到当前这个接口调用中。这种方式我并不是很推荐,因为这样一来,这个接口的测试就必须依赖登录接口,如果登录接口出了问题,就会造成这个接口的测试统统失败。不符合测试独立性的原则。当然,如果实在没有办法绕过,这种方法也是可以考虑的。

另一种,就是根据登录获取Token的实际原理,绕过登录接口。在这个例子中,服务端是通过登录成功后,生成一个Token存入redis中,作为key,并把用户的一些信息序列化成一个json串作为value。当需要Token的接口请求进来是,就去redis中找到对应的进行校验。那么我们可以直接往redis中写入Tokenjson值,那么用这个Token就直接可以通过校验了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// redis mod.rs
#[derive(Deserialize, Serialize, Clone)]
pub struct UserInfo {
    #[serde(rename = "accountId")]
    pub account_id: u32,
    #[serde(rename = "accountName")]
    pub account_name: String,
    pub cellphone: String,
    pub gender: u8,
}

impl UserInfo {
    pub fn new(account_id: u32, account_name: String, cellphone: String, gender: u8) -> Self {
        Self {
            account_id,
            account_name,
            cellphone,
            gender,
        }
    }
    
    pub fn prepare_login_user(self, url: &str, token: &str) -> Result<(), Box<dyn std::error::Error>>{
        let client = redis::Client::open(url)?;
        let mut con = client.get_connection()?;
        let value = serde_json::to_string(&self)?;
        let _ : () = con.set(token, value)?;
        Ok(())
    }
}

我们这样就可以使用prepare_login_user方法直接实现登录,而不需要去调用登录接口,解除了对其他接口的耦合性。

另外,调用这个接口,需要在数据库中已经存在一张订单,才可以修改订单地址。这也有多种方式实现。

一种是先调用创建订单接口,把订单准备好,再调用当前这个修改订单地址的接口。但是同样的道理,也会造成接口间的依赖。

另一种,就是在测试环境准备时,数据库就事先埋好一些数据,包括用于这个测试的订单。这种环境准备时埋入的数据,我称为铺底数据,适合于不太会变化,大部分案例都需要用的数据。比如一些基础用户信息,一些产品信息等等。而这个订单信息,可能就这一个case会用,并且会被这个接口变更,我们不可能在这个案例执行一边后,就重新执行铺底数据的初始化,这样每次执行显得太了。

这类数据,我更推荐在案例执行之前,写在方法的init中,单独为这个案例准备一条数据,案例执行完成后,就清理掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// modify.rs

    fn init_db(db_conf: &MySQLConfig, account_id: u32, order_number: &str) {
        info!("init db data");
        let mut conn = db_conf.get_conn();
        let result = conn.exec_drop(
            r"INSERT INTO `t_order` (`orderNbr`, `orderStatus`, `buyerId`, `address`, `createTime`, `updateTime`) 
                VALUES (?, ?, ?, ?, now(), now());",
            (
                order_number,
                0,
                account_id,
                "Streat Z, Pudong district, Shanghai",),
        );
        assert!(result.is_ok(), "init db failed");
    }

    fn clean_db(db_conf: &MySQLConfig, order_number: &str) {
        info!("clean db data in `t_order`");
        let mut conn =db_conf.get_conn();
        let result = conn.exec_drop(
            r"DELETE FROM `t_order` WHERE `orderNbr` = ?;",
            (order_number,),
        );
        assert!(result.is_ok(), "clean db failed");
    }

这两个方法放在测试模块中待被调用。

现在可以正式写这个测试了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// modify.rs

    #[tracing_test::traced_test]
    #[rstest]
    #[case("s001", "Streat A, Pudong district, Shanghai", 1000, "Modify address success")]
    #[tokio::test]
    async fn test_modify_success(#[case] order_nbr: &str, #[case] address: &str, #[case] code: u32, #[case] message: &str) {
        info!("case: test_modify_success start, with params: order_nbr: {}, address: {}", order_nbr, address);
        // ---- 初始化测试数据 ----
        let env = super::super::get_env();
        info!("get env: {:?}", env);
        let mysql_conf = MySQLConfig::new(
            env.db_conf.host.clone(), 
            env.db_conf.port,
            env.db_conf.username.clone(), 
            env.db_conf.password.clone(), 
            "coffeedb".to_string(),
        );
        let token: &str = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJsaXVkYW8iLCJleHAiOjE2NzA5ODkyNDV9.aegReDbly0asm4lC6aOCvn1gW26_cFGqmxqBeV-JI90";
        let user_info = UserInfo::new(
            1, 
            "liudao".to_string(), 
            "13511111111".to_string(), 
            0,
        );

        init(&env.redis_conf.url, &mysql_conf, token, user_info.clone(), order_nbr);

        // ---- 执行测试 ----
        // 发起请求
        let resp = modify(&env.url, token, order_nbr, address).await.unwrap();

        // ---- 验证结果 ----
        // 获取数据库数据
        let mut conn = mysql_conf.get_conn();
        let record = conn.exec_map(
            r"SELECT `orderNbr`, `orderStatus`, `buyerId`, `address` FROM `t_order` WHERE `orderNbr` = ?", 
            (order_nbr,), 
            |(order_nbr, order_status, buyer_id, address): (String, u8, u32, String)| {
                OrderInfo {
                    order_nbr,
                    order_status,
                    buyer_id,
                    address,
                }
            },
        ).unwrap();

        let result = panic::catch_unwind( || {
            //返回结果断言
            assert_eq!(resp.code, code);
            assert_eq!(resp.message, message);

            // 数据库记录断言
            assert_eq!(record.len(), 1);
            assert_eq!(record, vec![OrderInfo {
                order_nbr: order_nbr.to_string(),
                order_status: 0,
                buyer_id: user_info.account_id,
                address: address.to_string(),
            }]);
        });
        
        // ---- 清理测试数据 ----
        clean(&mysql_conf, order_nbr);

        if let Err(e) = result {
            panic::resume_unwind(e);
        }
       

    }

在这个测试案例中,有数据参数化,有日志输出,有init准备,有clean清理测试数据,有接口调用,有结果校验。

可以通过在执行时加入环境变量ENV=xxx来指定被测环境,允许在多个测试环境间切换。

并且在结果校验中,包含了请求返回数据的校验,以及影响的数据库字段的校验。

这就是一个相当完整的接口测试案例了。

写在结尾

当然,这里只是用rust举了一个比较典型的例子,工作中一定还会遇到各种特殊的情况,多考虑案例的独立性,并且满足编程的基本原则,就不会出大的问题。

正是因为有这么多需要考虑的点,所以要做一个接口测试的低代码或者零代码平台,需要处理的细节太多了,没有几年的打磨,很难满足这么多要求。而我所展示的,只是接口测试的基本要求,任何一条不满足,这个接口测试就是存在问题的。所以使用代码形式来写接口测试,是最容易满足这一系列条件的。

如果您是使用其他编程语言来实现接口自动化测试的,也需要考虑这些条件。既然要写代码,那就请把它写好,写的像个样子。

完整代码,请看GitHub传送门。

https://github.com/jimmyseraph/api-test-demo


资源分享

下方这份完整的软件测试视频学习教程已经上传CSDN官方认证的二维码,朋友们如果需要可以自行免费领取 【保证100%免费】

这些资料,对于想进阶【自动化测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!凡事要趁早,特别是技术行业,一定要提升技术功底。希望对大家有所帮助……基础知识、Linux必备、Shell、互联网程序原理、Mysql数据库、抓包工具专题、接口测试工具、测试进阶-Python编程、Web自动化测试、APP自动化测试、接口自动化测试、测试高级持续集成、测试架构开发测试框架、性能测试、安全测试等配套学习资源免费分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/95370.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

selenium三大等待

使用场景&#xff1a;有时候当我们操作页面元素时&#xff0c;需要等待这个过程才能操作成功。 做Ui自动化的时候&#xff0c;考虑到稳定性&#xff1a;多次运行同一脚本&#xff0c;都能够保证它是成功的。 一、强制等待&#xff1a;sleep(秒) 比如sleep(10)&#xff0c;就…

程序员职业自由的6大阶段,你还在格子间写代码吗?

技术群里聊天&#xff0c;总会提到自由职业。每每说起&#xff0c;大家都很羡慕&#xff0c;觉得自由职业者就等于“想干什么干什么&#xff0c;想怎么赚钱怎么赚钱”&#xff0c;而自己却还在格子间写代码。为此&#xff0c;有人还专门列了一下程序员职业自由的6大阶段。 程序…

Flask 引入swagger

1.使用flasgger Flasgger是 flasgger 组织在Github上开源的解析和渲染 SwaggerUI 的 Flask 拓展。 提供了对于Swagger文档标准的解析和SwaggerUI的生成&#xff0c;支持使用YAML、Python字典和Marshmallo、Schema的定义。 支持使用JSON Schema进行数据验证&#xff0c;支持F…

Docker Desktop 向大公司宣告收费,网友大呼:是时候弃用了

在容器引擎 Docker 诞生的 8 年间&#xff0c;其与开源的容器编排 Kubernetes 共同推动容器技术在云计算领域的应用&#xff0c;也让自身在全球范围内受到了广泛的关注。可以说&#xff0c;做过云计算开发的程序员&#xff0c;十有八有学过 Docker 技术。 不过&#xff0c;近日…

功率计和频谱仪测量功率的差异

在射频、微波无线系统中准确的测量功率是最基本的要求&#xff0c;进行功率测量有多种测量设备和测试方法可以选择&#xff0c;如功率计测量、频谱测量等。在实际测试工作中&#xff0c;应确保每种方法的优点和局限性不会影响测试数据的准确性。 本文将探讨不同测试方法之间的…

ssh+mysql实现的Java web论坛系统源码+视频运行教程+参考论文+开题报告

今天给大家来演示一下一款有springstruts2hibernatemysql实现的Java web论坛系统源码&#xff0c;本系统功能类似与csdn论坛&#xff0c;用户发帖时可以设置积分&#xff0c;当结帖时可以选择给评论的用户给与相应的分值&#xff0c;功能非常完善&#xff0c;已经接近可以直接上…

经验分享:应届生如何入行软件测试?我学习3个月成功上岸12K

推荐阅读: [内部资源] 想拿年薪30W的软件测试人员&#xff0c;这份资料必须领取~ Python自动化测试全栈性能测试全栈&#xff0c;挑战年薪40W 从功能测试进阶自动化测试&#xff0c;熬夜7天整理出这一份超全学习指南【附网盘资源】 正文 软件测试工程师 XXX / 应届毕业生 …

开源BI报表工具Metabase初体验

概述 一款开源BI工具&#xff0c;后端是用clojure开发的。官网&#xff0c;GitHub。 安装 安装非常简单&#xff0c;以metabase.jar包&#xff08;后文简称jar包&#xff09;方式分发。在GitHub Release页面下载最新版即可。也可以通过wget命令下载&#xff1a; wget http:/…

力扣383.赎金信(java语言散列表法)

题目描述&#xff1a; 给你两个字符串&#xff1a;ransomNote 和 magazine &#xff0c;判断 ransomNote 能不能由 magazine 里面的字符构成。 如果可以&#xff0c;返回 true &#xff1b;否则返回 false 。 magazine 中的每个字符只能在 ransomNote 中使用一次。 解题思…

KVM - 虚机内核配置

缘起 笔者最近分别购买了一台腾讯云和百度云的机器&#xff0c;都是一年期的&#xff0c;配置和价格分别如下&#xff1a; 腾讯云百度云配置2 核&#xff0c;2G 内存&#xff0c;40G 硬盘2 核&#xff0c;4G 内存&#xff0c;80G 硬盘价格50 元78 元 似乎性价比都差不多&…

802.11协议:wifi

802.11协议 博客链接&#xff1a;https://www.blog.23day.site/articles/71 一、协议简介 IEEE 802协议簇是指IEEE标准中关于局域网&#xff08;LAN&#xff09;和城域网&#xff08;MAN&#xff09;的一系列标准。IEEE 802中定义的服务和协议限定在OSI七层网络模型的最低两层…

m基于FPGA的NBDP系统ARQ单元模块的verilog实现

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 NBDP&#xff08;窄带直接印字电报&#xff09;&#xff0c;全称Narrow-Band Direct-Printing。是GMDSS地面无线民系统中的一种重要通信技术&#xff0c;这个终端设备&#xff0c;要与MF、HF设备…

阿里云国际版代充-阿里云无影云桌面是什么?

阿里云国际版无影云桌面的原产品名为弹性云桌面&#xff0c;融合了无影产品技术后更名升级。它可以为用户提供易用、安全、高效的云上桌面服务&#xff0c;帮助快速构建、高效管理桌面办公环境&#xff0c;提供安全、灵活的办公体系。 阿里云国际版无影云桌面分类&#xff1a;…

微信商城怎么开通【微信商城】

微信商城又可以称为微商城&#xff0c;微信商城是很多商家企业作为线上销售的一个平台&#xff0c;毕竟在微信这个这么高日活量的社交平台上&#xff0c;自然会有很多潜在顾客。那么如果想在微信上卖货的商家企业怎么开通微信商城呢&#xff0c;下面给大家说说微信商城怎么开通…

计算机网络实验二:ARP欺骗

计算机网络实验二&#xff1a;ARP欺骗 博客链接&#xff1a;https://www.blog.23day.site/articles/66 一、wireshark 要求&#xff1a;配置并使用wireshark&#xff0c;在无线环境下监听非本机的数据码流&#xff0c;记录并解释如下集中情况下听到的数据包的意义&#xff0c;…

Python爬取福利图要在中午,因为早晚会出事!我才存了2000文件夹

爱美之心人皆有之&#xff0c;我们爬取找寻这些漂亮小姐姐的图片并不是出于什么龌龊的目的。而是欣赏美好的东西总是会让人心情愉悦的&#xff0c;对于美丽事物的追求是人与生俱来的天性。 爬虫成果 发现很多想要学习Python却不知道该怎么下手的朋友&#xff0c;正好我这里整…

基于场景的数据集------明厨亮灶数据集

为了和各位开发爱好者深入合作交流&#xff0c;特此准备分批次开放数据集拱大家交流学士研究使用&#xff0c;整理的非常细腻&#xff0c;有些是专业队伍标注的&#xff0c;主要是菲律宾那边的团队进行标注的。依据众多算法搭建的算法平台主体算法包括 人脸识别&#xff0c;人…

Vue3-ElemenPlu,全栈开发后台系统-JWT方案讲解第三章-Koa架构设计接口方面实现mongdb安装配置工具函数的封装前台首页实现

第三章-Koa架构设计 #!/usr/bin/env node/*** Module dependencies.*/var app = require(../app); var debug = require(debug)

公司新来的00后真是卷王,工作没两年,跳槽到我们公司起薪18K都快接近我了

都说00后躺平了&#xff0c;但是有一说一&#xff0c;该卷的还是卷。这不&#xff0c;前段时间我们公司来了个00后&#xff0c;工作没两年&#xff0c;跳槽到我们公司起薪18K&#xff0c;都快接近我了。后来才知道人家是个卷王&#xff0c;从早干到晚就差搬张床到工位睡觉了。 …

【LVGL学习笔记】(一)环境搭建

LVGL全程LittleVGL&#xff0c;是一个轻量化的&#xff0c;开源的&#xff0c;用于嵌入式GUI设计的图形库。并且配合LVGL模拟器&#xff0c;可以在电脑对界面进行编辑显示&#xff0c;测试通过后再移植进嵌入式设备中&#xff0c;可以高效地进行开发。 一.嵌入式设备的移植 L…