12306 项目编写测试用例+接口测试+自动化测试

为了综合将测试部分的知识点学以致用,本文结合 12306官网拿个Offer 12306项目 ,集中在 功能测试(XMind提测+用例编写)、接口测试(Java+TestNG、Python+Pytest、Apifox工具)、自动化测试 等方面(性能测试涉及较多资源知识储备,笔者不具便不展开)。
😊若有错误或者读者有更便捷的思路、操作等等,敬请批评指出。


前置知识

  • 用例的撰写方法

软件测试教程,这个视频的笔记和讲解过程适合新手

  • HTML基本结构,如div、内部的name、class、id属性之类。

如果是科班的,稍微复习一下就够用了

  • TestNG、Pytest、Selenium等技术(相关内容内会附上参考文章)
  • Apifox 测试工具
  • 接口测试用了 拿个Offer 12306项目 项目,读者也可以 Git 开源代码和看公开的部分文档进行调试(基本够用,当然项目的内部的技术文章介绍挺不错的👍)

在这里插入图片描述

一、功能测试

1. 用户模块

1.1 注册功能

📃 用户名规则: 由6-30位字母、数字或“_”两钟类型以上组合而成,且开头必须为字母,未注册

在12306官方网中,用户名相当于人的身份证号一样,对用户来说是唯一且不可更改的。

  • 如果注销后,曾用的用户名会变为可用
  • 在项目中,将注销的用户名添加到,实现注销用户名后续可用的功能,因此针对这一特殊实现添加了测试用例。

1. 等价类划分法

从需求中去划分要测试的类别,6-30位字母、数字或“_”包含了【长度】【类型】的类别,两钟类别以上组合,开头必须为字母,未注册过 为【特殊规则】,则在类别中去划分,得出有效数据和无效数据的规则以及具体测试数据。步骤如下:

1️⃣ 明确需求

由6-30位字母、数字或“_”两钟类别以上组合而成,且开头必须为字母

2️⃣划分有效等价和无效等价

有效等价无效等价
6-30位< 6、> 30位
字母、数字或“_”其他字符,如¥%&
两类型以上组合纯字母、纯数字、纯“_”等一种类型
开头为字母开头不为字母
未注册已注册

3️⃣ 提取数据编写测试用例

用例的撰写一般需要结合等价类和边界值两种方法,此处不先给出用例。

2. 边界值法

根据“开内闭外”原则,目标数据的范围为[6,30],则:

  • 上点:630
  • 内点:15(自由取)
  • 离点:5、7(舍去)、29(舍去)、31

使用 XMind 提取测试点:

在这里插入图片描述

  • 图中的具体测试数据和黑框的概要一般不用画,此处是为了方便笔者自己理解

使用 Excel 设计用例:

在这里插入图片描述

  • Excel部分用例的截图

复制部分 Excel 用例如下:

用例编号用例标题项目/模块优先级前置条件测试步骤用例数据预期结果
UserName-001合法用户名(6位(字母、数字“_”,字母开头组成)+未注册)User-RegisterP0进入注册界面1.在用户名输入框输入用户名
2.点击其他任意框
a_1234合法,无错误提示
UserName-002合法用户名(30位(字母、数字“_”,字母开头组成)+未注册)User-RegisterP0进入注册界面1.在用户名输入框输入用户名
2.点击其他任意框
b_1234567890_12
34567890_123456
合法,无错误提示
UserName-003合法用户名(15位(字母、数字“_”,字母开头组成)+未注册)User-RegisterP0进入注册界面1.在用户名输入框输入用户名
2.点击其他任意框
c_1234567890_12合法,无错误提示
……

1.2 登录功能

  • 登录先检验输入的 用户名/手机号/邮箱 是否符合规定的格式规范符合规范的账号+符合位数的密码才会进行下一步的匹配判断是否登录成功。
  • 账号不存在、账号存在密码错误 两者登录的提示相同,即不会另外提示账号不存在。

使用 XMind 提取测试点:

在这里插入图片描述

注意:逆向用例编写时,如下图中的红框内容在熟悉流程后都可以省去,仅是为了避免粗心写错用例测试数据。

  • 在创建逆向用例时,可以代入高中物理的 控制变量法,如本登录逆向用例中的“密码错误”,除了“密码”这一变量,其他变量一定是正向的,比如测试的账号一定是符合规范格式、已经注册的。

在这里插入图片描述

2. 购票模块

在这里插入图片描述
完整的XMind 提取测试点的图片1 在文末。

二、接口测试【登录】

📖 接口文档为 12306项目接口文档

1. 代码实现

1)Java +TestNG 实现

以【登录用例】为例,代码基本搭建参考 TestNG 单元测试框架的入门与实践 文章,注意添加依赖代码即可。

  • pom.xml 完整依赖代码
    <dependencies>
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>6.10</version>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>5.2.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>20210307</version>
        </dependency>
    </dependencies>

Java 代码

登录接口要求:
	1、接口:/api/user-service/v1/login
	2、Header:特定token值
	3、Body (JSON格式):
		{
			 "usernameOrMailOrPhone": "admin"
			 "password": "admin123456",

		}
  • @BeforeSuite注解:执行所有测试执行前运行一次添加测试的基础URI
  • @DataProvider注解:提供多组测试数据,可以模拟不同的测试场景
  • @Test注解:实际执行测试的方法,指定使用 @DataProvider 提供的测试数据
  • 构建请求体:根据传入的参数构建JSON格式的请求体
  • 发送请求:使用REST Assured构建POST请求,设置请求头(包括认证头部)和请求体,然后发送请求到登录端点。
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.json.JSONObject;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static io.restassured.RestAssured.given;
import static io.restassured.http.ContentType.JSON;

public class TestNG_12306 {
    
    private static final String BASE_URL = "http://localhost:8080";
    private static final String LOGIN_ENDPOINT = "/api/user-service/v1/login";
    private static final String AUTHORIZATION_TOKEN = "token值";
    
    @BeforeSuite
    public void setup() {
        RestAssured.baseURI = BASE_URL;
    }
    
    // 使用 @DataProvider 提供测试数据
    @DataProvider(name = "loginData")
    public Object[][] provideLoginData() {
        return new Object[][] {
                // 有效的用户名和密码
                {"admin", "admin123456", 200},
                // 有效的用户名和空密码
                {"admin", "", 200},
                // 空用户名和随机密码
                {"", "110110110", 200}
                // 可以继续添加测试用例数据
        };
    }
    
    // 测试登录接口
    @Test(dataProvider = "loginData")
    public void testLoginWithCredentials(String usernameOrMailOrPhone, String password, int expectedStatus) {
        String jsonBody = String.format("{\"usernameOrMailOrPhone\":\"%s\", \"password\":\"%s\"}", usernameOrMailOrPhone, password);
        Response response = given()
                .contentType(JSON)
                .header("Authorization", AUTHORIZATION_TOKEN) // 设置Authorization头部
                .body(jsonBody) // 设置请求体
                .when()
                .post(LOGIN_ENDPOINT);
        printFormattedResponse("JSON请求体:\n" ,jsonBody);
        response.then()
                .statusCode(expectedStatus); // 期望的状态码
       
        printFormattedResponse("JSON响应体:\n" ,response.getBody().asString());
    }
    // 控制台打印JSON输出,为了便于查看自定义这个方法美化了一下
    public static void printFormattedResponse(String title, String responseBody){
        // 先转为JSON格式
        JSONObject jsonObject = new JSONObject(responseBody);
        // 使用JSONUtils格式化JSON字符串
        // 第二个参数是缩进空格数
        String formattedJson = jsonObject.toString(4);
        // 打印美化后的JSON响应体
        System.out.println(title + formattedJson);
    }
}
  • 控制台输出结果
JSON请求体:
{
    "password": "admin123456",
    "usernameOrMailOrPhone": "admin"
}
JSON响应体:
{
    "code": "0",
    "data": {
        "realName": "徐万里",
        "accessToken": "返回的token值",
        "userId": "1683025552364568576",
        "username": "admin"
    },
    "requestId": null,
    "success": true,
    "message": null
}
JSON请求体:
{
    "password": "",
    "usernameOrMailOrPhone": "admin"
}
JSON响应体:
{
    "code": "B000001",
    "data": null,
    "requestId": null,
    "success": false,
    "message": "账号不存在或密码错误"
}
JSON请求体:
{
    "password": "admin123456",
    "usernameOrMailOrPhone": ""
}
JSON响应体:
{
    "code": "B000001",
    "data": null,
    "requestId": null,
    "success": false,
    "message": "账号不存在或密码错误"
}

===============================================
Default Suite
Total tests run: 3, Failures: 0, Skips: 0
===============================================

在这里插入图片描述

  • 美化的 TestNG 输出结果

2)Python + Pytest 实现

使用 Pytest fixture,将测试数据存储在 JSON 文件中,fixture 负责加载这些数据。

参考 Python测试框架之pytest详解 ,注意命名必须满足test_*.py格式或*_test.py格式。

  • 测试数据文件 test_data.json
[
    {
        "usernameOrMailOrPhone": "admin",
        "password": "admin123456",
        "expectedStatus": 200
    },
    {
        "usernameOrMailOrPhone": "admin",
        "password": "",
        "expectedStatus": 200
    },
    {
        "usernameOrMailOrPhone": "",
        "password": "admin123456",
        "expectedStatus": 200
    }
]
  • test_login.py
import requests
import json
import pytest

# 基础配置
BASE_URL = "http://localhost:8080"
LOGIN_ENDPOINT = "/api/user-service/v1/login"
AUTHORIZATION_TOKEN = "token值"


# fixture用于读取测试数据文件test_data.json,module参数表示在整个模块的测试中只会被调用一次
@pytest.fixture(scope="module")
def login_data():
    with open("test_data.json", "r") as file:
        return json.load(file)


# 测试登录接口
# 测试函数遍历每组测试数据,构建请求头和请求体,然后发送POST请求到登录端点
def test_login_with_credentials(login_data): 
    for data in login_data:
        headers = {
            "Content-Type": "application/json",
            "Authorization": AUTHORIZATION_TOKEN
        }
        payload = {
            "usernameOrMailOrPhone": data["usernameOrMailOrPhone"],
            "password": data["password"]
        }
        response = requests.post(
            url=f"{BASE_URL}{LOGIN_ENDPOINT}",  # 确保这里没有语法错误
            headers=headers,
            json=payload  # 使用json参数自动设置请求体和Content-Type
        )

        # 打印请求体
        print("JSON请求体:\n", json.dumps(payload, indent=4))

        # 验证响应状态码
        assert response.status_code == data["expectedStatus"]

        # 打印响应体
        print("JSON响应体:\n", json.dumps(response.json(), indent=4))
        print("------------------------------------------手动分割线------------------------------------------")


# 运行测试
if __name__ == "__main__":
    pytest.main("test_login.py")
  • 控制输出效果

在这里插入图片描述

起初运行的时候,控制台不打印任何信息,后面调了配置和改成上述的代码就可以了,如果读者遇到的话,可以搜关键词 Pytest 实时标准输出和捕获标准输出 解决。

2. Apifox工具使用

接口测试有很多工具,如 Postman,而 Apifox 较为全面,涵盖大部分测试工具的功能。

虽然接口文档 12306项目接口文档 在 Apifox 并且公网可以访问,但不能导出链接至本地 Apifox 使用(笔者的在线调试有问题),所以采用 查看接口信息+在本地 Apifox 建立项目输入有关信息 后调试。
接口测试思路可以参考 Postman 接口测试

一般来说,输入框会根据实际需求在前端进行设限,但不排除可通过抓包等方式发送给后端,所以前文所提及的功能测试中,很多情况在接口测试中依旧需要覆盖。

  • Excel 编写测试用例(部分)
用例编号项目模块用例标题请求URL请求类型/方法前置条件用例描述请求参数类型请求参数预期结果测试结果
User001用户服务用户登录/api/user-service/v1/loginPOST网络正常用户正常登录json{
“usernameOrMailOrPhone”: “admin”,
“password”: “admin123456”
}
{
“code”: “0”,
“data”: {
“realName”: “徐万里”,
“accessToken”: “”,
“userId”: “1683025552364568576”,
“username”: “admin”
},
“requestId”: null,
“success”: true,
“message”: null
}
{
“code”: “0”,
“data”: {
“realName”: “徐万里”,
“accessToken”: “”,
“userId”: “1683025552364568576”,
“username”: “admin”
},
“requestId”: null,
“success”: true,
“message”: null
}
User002用户服务用户登录/api/user-service/v2/loginPOST网络正常输入不存在的用户名和任意密码json{
“usernameOrMailOrPhone”: “abc110”,
“password”: “admin123457”
}
{
“code”: “B000001”,
“message”: “账号不存在或密码错误”,
“data”: null,
“requestId”: null,
“success”: false
}
{
“code”: “B000001”,
“message”: “账号不存在或密码错误”,
“data”: null,
“requestId”: null,
“success”: false
}
User003用户服务用户登录/api/user-service/v3/loginPOST网络正常输入有效用户名和错误密码json{
“usernameOrMailOrPhone”: “admin”,
“password”: “admin123456789”
}
{
“code”: “B000001”,
“message”: “账号不存在或密码错误”,
“data”: null,
“requestId”: null,
“success”: false
}
{
“code”: “B000001”,
“message”: “账号不存在或密码错误”,
“data”: null,
“requestId”: null,
“success”: false
}

Apifox 的接口测试和Postman一样较为简单,即编辑好接口,点击发送即可返回响应体,这里不赘述,网上很多教程。
虽然 Apifox 可以实现批量运行测试用例,但这种方式是要手动创建很多用例,较为便捷的是导入数据文件进行测试,这部分也属于自动化测试,放到下文展开。

三、自动化测试【购票 | 注册 | 登录】

📖 使用 JavaSelenium 技术栈,具体搭建过程可参考 Selenium+Java+Chrome+IDEA 自动化测试 文章(搭建完才可以进行具体测试)。

1. 元素定位常见语法介绍

// 方式1
chromeDriver.findElement(By.id("绑定此id的元素的id")).sendKeys("填写值");
// 方式二
chromeDriver.findElementById("绑定此id的元素的id").sendKeys("填写值");
// 点击该元素
chromeDriver.findElement(By.id("绑定此id的元素的id")).click();
// 点击通过该地址找到的元素
chromeDriver.findElement(By.xpath("可定位到元素的地址")).click();

上面为较常用的类型,同理可以举一反三,除了id,还可以通过 class、name 等等元素实现找 到HTML 元素,在 IDEA 中通过代码提示输入。

2. XPATH 简要说明

可以参考 元素定位之XPath定位-学习 文章,里面较为全面介绍关于 XPATH 的语法。

  • 元素X/ 表示 X 的下一层级(即从当前层往下走一层),后面可以接下一层的任意元素
    • 元素X/* 表示下一层级的所有元素
    • 元素X/div[@class='cal-cm'] 表示在 X 的下一层级中,符合条件 class = 'cal-cm' 的div
    • 元素X/*[1] 表示 X 的下一层级中,不限类型的第一个子元素
    • 元素X/.. 表示 X 的上一层级,相当于父元素
    • 元素X//a 双杠(此时双杠视为整体)表示寻找 X 下所有符合条件的a(所有子孙)

3. 操作步骤

1️⃣ F12 进入代码调试,查看目标测试输入框的元素,如 class id属性

2️⃣ 若是通过 XPath 方式寻找元素,则先编写元素地址

3️⃣ Ctrl+F 调出搜索接口,输入自己编写的 XPath 地址测试是否能定位到目标元素

🎈 以购买单程为例://li[@id='J-chepiao']//a[text()='单程']

在这里插入图片描述

当然也有如下更直观便捷的方法://*[@id="megamenu-3"]/div[1]/ul/li[1]/a
在这里插入图片描述

  • 个人认为小白可以尝试自己编写,只有自己写一遍才知道自以为的“理想”和“正确”答案的差距

4. Java+Selenium 完整代码

如果想直观看到每一行代码执行过程,可以选择:

  • 每行操作语句后加入Thread.sleep(3000);代码
  • 在方法名添加调试点,自己点击一行一行调试
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.Select;

public class SeleniumTest_12306 {
    static ChromeDriver chromeDriver;
    public static void main (String[] args) throws InterruptedException {
        openChrome();
        // register(chromeDriver);
        login(chromeDriver);
        buyTicket(chromeDriver);
        
    }
    /**
     * 业务:购买明天从广州南去深圳北的高铁,不限座位类型,学生票,乘车人为登录人
     * @param chromeDriver
     */
    public static void buyTicket(ChromeDriver chromeDriver){
        // 单程的车票购买
        // HTML结构:车票所在的li(包含着<a>、<div>) -> <div>内文字为'单程'的<a>
        chromeDriver.findElement(By.xpath("//li[@id='J-chepiao']//a[text()='单程']")).click();
        // 出发站
        chromeDriver.findElement(By.id("J-fromStationText")).sendKeys("广州南");
        // 目的站
        chromeDriver.findElementById("toStationText").sendKeys("深圳北");
        // HTML的日期选择结构:本月class='cal'和下一个月class='cal cal-right'两个视图,如本月30天,则有30个<div>,30个里面嵌套的<div>才是我们要的
        // 如果今天在本月月末(即'今天'的下一个元素不存在),则第二天需去cal-right的div内找;若不是,则直接访问'今天'的下一个元素
        WebElement nextDate  = chromeDriver.findElementByXPath("//*[@class='cal']//div[text()='今天']/../following-sibling::*[1]");
        if (nextDate == null){
            // 即'今天'属于本月最后一天,要去下个月找
            chromeDriver.findElement(By.xpath("//*[@class='cal cal-right']/div[@class='cal-cm']/*[1]")).click();
        } else {
            nextDate.click();
        }
        // 选择任意有票的车次->预订
        // 在官网中,有票的单元格的class = 'yes',找到符合条件的单元格td -> 返回父级tr -> 寻找最后一个'td'(包含预订按钮)
        chromeDriver.findElement(By.xpath("//td[contains(@class,'yes')]/../td[last()]/a")).click();
        // 选择受让人(登录人)
        chromeDriver.findElement(By.id("djPassenger_0")).click();
        // 登录人账号内通过验证的乘车人
        // chromeDriver.findElement(By.id("normalPassenger_2")).click();
        // 票类型,即几等座
        Select selectTicket = new Select(chromeDriver.findElement(By.id("ticketType_1")));
        // value = 3为学生
        selectTicket.selectByValue("3");
        // 座位类型
        Select selectSeat = new Select(chromeDriver.findElement(By.id("seatType_1")));
        // 这里的座位与对应的value值并不固定(有时是1、2、3、m等等,对应座位的类别也很多)
        selectSeat.selectByValue("1");
        // 提交订单submitOrder_id
        chromeDriver.findElement(By.id("提交订单submitOrder_id")).click();
    }
    public static void login(ChromeDriver chromeDriver){
        chromeDriver.findElement(By.id("J-userName")).sendKeys("手机号码");
        chromeDriver.findElement(By.id("J-password")).sendKeys("密码");
        chromeDriver.findElement(By.id("J-login")).click();
        chromeDriver.findElement(By.id("id_card")).sendKeys("身份证末四位");
        chromeDriver.findElement(By.id("verification_code")).click();
        chromeDriver.findElement(By.id("code")).sendKeys("手机收到的验证码");
        chromeDriver.findElement(By.id("sureClick")).click();
    }
    public static void register(ChromeDriver chromeDriver){
        chromeDriver.findElement(By.className("txt-primary")).click();
        chromeDriver.findElement(By.id("userName")).sendKeys("a123456");
        chromeDriver.findElement(By.id("passWord")).sendKeys("abc123");
        chromeDriver.findElement(By.id("confirmPassWord")).sendKeys("abc123");
        chromeDriver.findElement(By.id("regist_name")).sendKeys("姓名");
        chromeDriver.findElement(By.id("cardCode")).sendKeys("18位身份证号");
        Select select = new Select(chromeDriver.findElement(By.id("passengerType")));
        // 乘客类型,3表示学生
        select.selectByValue("3");
        chromeDriver.findElement(By.id("mobileNo")).sendKeys("手机号码");
        chromeDriver.findElement(By.id("checkAgree")).click();
        chromeDriver.findElement(By.id("nextBtn")).click();
    }
    public static void openChrome(){
        System.setProperty("webdriver.chrome.driver","自己电脑的谷歌驱动地址");
        ChromeOptions options = new ChromeOptions();
        // 表示允许所有请求
        options.addArguments("--remote-allow-origins=*");
        chromeDriver = new ChromeDriver(options);
        chromeDriver.get("https://kyfw.12306.cn/otn/resources/login.html");
    }
}

5. Apifox 的自动化测试

前置工作:在Apifox 建立项目、创建接口(这里不加以演示)
具体过程可参考 Apifox批量导入测试数据 文章。

  • 自动化测试界面,创建测试项目,修改请求体的传参部分代码

在这里插入图片描述

  • 导入JSON数据,这里笔者导入上文Python测试的JSON数据文件

在这里插入图片描述

  • 功能测试 -> 测试数据 选择刚刚上次命名的数据 -> 点击运行,结果如下,点开每个用例可以看到请求体和响应体内容。

在这里插入图片描述


附:通篇过程更侧重于将所学知识点、工具进行运用,虽然选择12306有关资源网站进行测试,但在企业正式测试的角度来说很多地方肯定远远不足,但学无止境,慢慢进步🔥


  1. 完整XMind测试点
    在这里插入图片描述 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值