【Web自动化测试】Python + Selenium实现携程网火车票订购的自动化测试——代码重构篇


【Web自动化测试】Python + Selenium实现携程网火车票订购的自动化测试之后,需要将原先复杂的代码进行重组,来让整体项目的代码更加简洁明了,也更利于操作。于是,我们需要对源代码进行代码重构并优化其中的功能。
整体来讲,我们需要将代码分为三层,也就是:

测试代码层 测试
逻辑代码层 逻辑
基础层 基础功能

1. 基础层

在此基础上,就要求我们对于代码的功能进行细化,于是我们可以将源代码中的诸多功能独立出来放置到基础层中,如下:

# base_functions.py
# @Layered Architecture : 基础层
import os
import sys
import json
import pandas as pd
from time import sleep
from ruamel.yaml import YAML
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from ruamel.yaml.scalarstring import DoubleQuotedScalarString

# 初始化Chrome浏览器驱动
driver = webdriver.Chrome()

yaml = YAML()
# 保留引号和注释
yaml.preserve_quotes = True

# 获取driver
def getDriver():
    return driver

# 基础浏览器操作
def openUrl(url):
    driver.get(url)
    # 最大化页面
    driver.maximize_window()

# 捕获异常方法【用于判断某元素是否存在】
def isElementPresent(xpath):
    try:
        driver.find_element(By.XPATH, xpath)
        return True
    except Exception:
        return False

# 关闭浏览器,释放资源
def close():
    print("关闭浏览器并释放资源")
    # driver.quit()

# 检索火车票
def searchTicket(departure, destination, date):
    # 获取出发站、到达站和日期

    # 定位元素【出发站、到达站、日期、搜索按钮】
    departureStation = driver.find_element(By.XPATH, "//*[@id='label-departStation']")
    arrivalStation = driver.find_element(By.XPATH, "//*[@id='label-arriveStation']")
    datePicker = driver.find_element(By.XPATH, "//*[@id='label-departDate']")
    searchBtn = driver.find_element(By.XPATH, "//*[text()='搜索']")

    # 输入出发站
    departureStation.send_keys(Keys.CONTROL, 'a')
    departureStation.send_keys(Keys.DELETE)
    departureStation.send_keys(f"{departure}")

    # 输入到达站
    arrivalStation.send_keys(Keys.CONTROL, 'a')
    arrivalStation.send_keys(Keys.DELETE)
    arrivalStation.send_keys(f"{destination}")

    # 使用 JavaScript 添加日期控件input样式
    js_code = """
    var styles = '.assist-block-dom, .assist-flex-dom, .assist-ib-dom { display: block !important; }';
    var styleSheet = document.createElement("style");
    styleSheet.type = "text/css";
    styleSheet.innerText = styles;
    document.head.appendChild(styleSheet);
    """
    driver.execute_script(js_code)

    # 输入日期
    datePicker.send_keys(Keys.CONTROL, 'a')
    datePicker.send_keys(Keys.DELETE)
    datePicker.send_keys(f"{date}")

    # 点击搜索按钮
    driver.execute_script("arguments[0].click();", searchBtn)

    # 隐式等待
    driver.implicitly_wait(5)
    print(f"本次出发站为:{departure},到达站为:{destination}")
    sleep(2)

# 滚动加载数据
def loadDataByScroll():
    # 滚动加载数据
    last_count = 0

    # 开启循环,确保获取到所有数据
    while True:
        # 获取当前卡片数量
        cards = driver.find_elements(By.XPATH, "//div[@class='card-white list-item']")
        current_count = len(cards)
        # 如果卡片数量没有变化,停止滚动
        if current_count == last_count:
            break
        # 滚动到底部
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        last_count = current_count
        # 等待新数据加载
        sleep(2)  # 根据加载速度调整等待时间

    # 将页面滚动到顶部,方便观察
    driver.execute_script("window.scrollTo(0, 0);")

# 筛选火车车次
def filterTickets(onlyAvailable, trainTypes, departTime, arrivalTime, seats, seatTypes):
    # 仅显示有票车次
    if onlyAvailable:
        driver.find_element(By.XPATH, "//*[text()='仅显示有票车次']").click()

    # 车型
    for type in trainTypes:
        if type['select']:
            driver.find_element(By.XPATH, f"//*[text()='{type['type']}']").click()

    # 筛选时间所需的变量
    time6 = datetime.strptime("06:00", "%H:%M").time()
    time12 = datetime.strptime("12:00", "%H:%M").time()
    time18 = datetime.strptime("18:00", "%H:%M").time()
    departTime = datetime.strptime(departTime, "%H:%M").time()
    arrivalTime = datetime.strptime(arrivalTime, "%H:%M").time()

    # 出发时间
    if departTime > time6:
        if departTime > time12:
            if departTime > time18:
                driver.find_element(By.XPATH,
                                    '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[3]/ul/li[4]''').click()
            else:
                driver.find_element(By.XPATH,
                                    '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[3]/ul/li[3]''').click()
        else:
            driver.find_element(By.XPATH,
                                '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[3]/ul/li[2]''').click()
    else:
        driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[3]/ul/li[1]''').click()

    # 抵达时间
    if arrivalTime > time6:
        if arrivalTime > time12:
            if arrivalTime > time18:
                driver.find_element(By.XPATH,
                                    '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[4]/ul/li[4]''').click()
            else:
                driver.find_element(By.XPATH,
                                    '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[4]/ul/li[3]''').click()
        else:
            driver.find_element(By.XPATH,
                                '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[4]/ul/li[2]''').click()
    else:
        driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[4]/ul/li[1]''').click()

    # 筛选坐席
    for seat in seats:
        if seat['type'] in seatTypes and seat['available']:
            driver.find_element(By.XPATH, f"//i[@class='ifont-checkbox']/following-sibling::strong[text()='{seat['type']}']/parent::li").click()

    # 隐式等待
    driver.implicitly_wait(5)

    # 根据时间进行再筛选
    cards = driver.find_elements(By.XPATH, "//section[@role='product']/div[@class='card-white list-item']")
    if len(cards) == 0:
        print("没有预期车票,关闭程序~")
        close()
    isValidTicket = False
    for card in cards:
        deTime = card.find_element(By.XPATH, ".//div/div[@class='from']/div[@class='time']").text
        arrTime = card.find_element(By.XPATH, ".//div/div[@class='to']/div[@class='time']").text
        if departTime <= datetime.strptime(deTime, "%H:%M").time():
            if arrivalTime >= datetime.strptime(arrTime, "%H:%M").time():
                print(f"此次列车出发时间为:{deTime}, 预计到达时间为:{arrTime}")
                isValidTicket = True
                cardId = card.get_attribute('id')
                break
    # 没有预期车票
    if isValidTicket == False:
        print("没有预期车票,关闭程序~")
        close()

    # 定位到预期车次card并展开
    driver.find_element(By.XPATH, f"//*[@id='{cardId}']/div[@class='list-bd']/button[@aria-label='展开或收起详情']").click()

    # 根据坐席优先级选择作为类型并进入订票页面
    for type in seatTypes:
        if isElementPresent(f"//div[@id='{cardId}']/following-sibling::div[1]/ul/li/strong[text()='{type}']/following-sibling::button[text()='预订']"):
            driver.find_element(By.XPATH,
                                f"//div[@id='{cardId}']/following-sibling::div[1]/ul/li/strong[text()='{type}']/following-sibling::button[text()='预订']").click()
            break

# 检查坐席种类是否和配置文件一致
def checkSeats(seatsConfig, data):
    # 打开筛选界面
    driver.find_element(By.XPATH, "//div[text()='展开']").click()
    sleep(1)

    print("开始检查坐席的配置信息~")

    # 获取所有坐席种类并保存在seatList中
    liList = driver.find_elements(By.XPATH, "//h4[text()='坐席']/parent::div[1]/following-sibling::ul/li")
    seatList = []
    for li in liList:
        seatList.append(li.find_element(By.XPATH, ".//strong").text)

    if len(seatList) == 0:
        print("没有坐席种类!无数据,关闭系统")
        close()

    # 获取config.yaml中的seats列表

    # 创建一个包含所有 seatList 中类型的 set
    seatSet = set(seatList)

    # 初始化标志变量,用于检查是否需要更新
    updateFlag = False

    # 遍历 config.yaml 中的 seats,检查是否满足条件:
    # 1. seatList 中的类型 available 均为 True
    # 2. 不在 seatList 中的类型 available 均为 False
    # 3. seatsConfig中的种类数量大于等于seatSet中元素的数量
    for seat in seatsConfig:
        type = seat['type']
        if type in seatSet:
            # 如果 seat 类型在 seatList 中,但是 available 不为 True,设置需要更新
            if not seat['available']:
                updateFlag = True
                break
        else:
            # 如果 seat 类型不在 seatList 中,但是 available 不为 False,设置需要更新
            if seat['available']:
                updateFlag = True
                break
    if len(seatsConfig) < len(seatSet):
        updateFlag = True

    if not updateFlag:
        print("坐席配置信息无误!")
    else:
        # 遍历 config.yaml 中的 seats,并根据 seatList 更新 available 状态
        for seat in seatsConfig:
            if seat['type'] in seatSet:
                # 如果 seatList 中存在该座位类型,将 available 设为 True
                seat['available'] = True
            else:
                # 如果 seatList 中不存在该座位类型,将 available 设为 False
                seat['available'] = False

        # 检查 seatList 中的类型是否都在 config.yaml 中
        for type in seatList:
            if not any(seat['type'] == type for seat in seatsConfig):
                # 如果 seatList 中的类型不在 config.yaml 中,添加新的座位类型
                seatsConfig.append({
                    'type': DoubleQuotedScalarString(type),  # 强制使用双引号
                    'available': True  # 默认 available 为 True
                })

        # 更新后的配置写回 config.yaml 文件
        with open('./config.yaml', 'w', encoding='utf-8') as f:
            yaml.dump(data, f)

        print("坐席配置信息已更新!")
        # 重新获取配置文件中的数据
        with open("./config.yaml", 'r', encoding='utf-8') as file:
            config = yaml.load(file)
        data = config

# 登录
def login(cookiesFilePath):
    # 判断cookies.txt是否存在及检测cookies.txt内是否有内容
    if os.path.exists(cookiesFilePath) and os.path.getsize(cookiesFilePath) > 0:
        print("将使用Cookies实现自动登录~")
        # 注入cookies
        with open(cookiesFilePath, 'r') as file:
            cookies = json.load(file)
        for cookie in cookies:
            # 添加 cookie 之前,删除 'expiry' 属性,防止不兼容问题
            if 'expiry' in cookie:
                del cookie['expiry']
            driver.add_cookie(cookie)
    else:
        print("请手动登录~")
        # 点击登录按钮
        loginBtn = driver.find_element(By.XPATH, "//button[@class='tl_nfes_home_header_login_not_frb4a']")
        loginBtn.click()
        # 隐式等待
        driver.implicitly_wait(10)
        # 切换登录方式为:扫码登录
        driver.find_element(By.PARTIAL_LINK_TEXT, "扫码登录").click()
        # 循环等待扫码完成
        while True:
            print("===========开始循环===========")
            if isElementPresent("//p[text()='二维码已失效']"):
                print("=========二维码已过期=========")
                sleep(3)
                refreshBtn = driver.find_element(By.XPATH, "//a[text()='刷新']")
                refreshBtn.click()
                sleep(3)
                print("=========二维码已刷新=========")
            elif isElementPresent("//*[@id='label-departStation']"):
                print("===========登录成功===========")
                break
            else:
                print("=========等待扫描QR码=========")
                sleep(10)
        cookies = driver.get_cookies()
        # 保存cookies到本地
        with open(cookiesFilePath, 'w') as file:
            json.dump(cookies, file)
            print("====cookies写入完成====")
    sleep(3)
    print("浏览器刷新~")
    driver.refresh()
    sleep(3)

def selectPassengers(nameList):
    driver.find_element(By.XPATH, "//button[contains(@class, 'btn-blue') and contains(@class, 'btn-add')]").click()
    # 显式等待iframe加载完成
    WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.XPATH, "//iframe[@id='picker-iframe']")))
    sleep(2)
    # 切换frame
    iframe = driver.find_element(By.XPATH, "//iframe[@id='picker-iframe']")
    driver.switch_to.frame(iframe)
    # 添加乘客信息
    for name in nameList:
        passengerCard = driver.find_element(By.XPATH,
                                            f"//span[text()='{name}']/ancestor::div[2][@class='PassengerCell_innerContainer__PPhbr']")
        passengerCard.click()
    sleep(1)
    confirmBtn = driver.find_element(By.XPATH, "//div[contains(@class, 'passengerList_bottomButton__VF11U')]")
    # driver.execute_script("arguments[0].click()", confirmBtn)
    confirmBtn.click()
    sleep(1)
    # 切换回上一个页面
    driver.switch_to.parent_frame()

def addPassengers(passengers):
    nameList = []
    for passenger in passengers:
        nameList.append(passenger['name'])
        if not isElementPresent(f"//li[.//div[@class='name' and text()='{passenger['name']}']]"):
            driver.find_element(By.XPATH, "//button[text()='新增乘客']").click()  # 点击新增游客
            driver.find_element(By.XPATH, f"//div[p[text()='{passenger['type']}']]").click()  # 选择乘客类型
            # 输入姓名
            driver.find_element(By.XPATH,
                                "//input[contains(@class, 'input-txt') and contains(@class, 'focus-cNName')]").send_keys(
                f"{passenger['name']}")
            # 输入身份证号
            driver.find_element(By.XPATH,
                                "//input[contains(@class, 'input-txt') and contains(@class, 'focus-identityNo')]").send_keys(
                f"{passenger['ID']}")
            # 输入手机号
            driver.find_element(By.XPATH,
                                "//input[contains(@class, 'input-txt') and contains(@class, 'focus-mobilePhone')]").send_keys(
                f"{passenger['tel']}")
            # 点击添加按钮
            driver.find_element(By.XPATH, "//button[text()='确认添加']").click()
    sleep(1)
    selectPassengers(nameList)

2. 逻辑层

进而在逻辑层中,调用基础层的方法并按照一定的逻辑实现预想的功能,具体逻辑层代码如下:

# xc_book.py
# @Layered Architecture : 逻辑层
import xctickets.base_functions as func
from time import sleep

def book_ticket(cookiesFilePath, row, data):
    url = "https://trains.ctrip.com/"
    # 初始化浏览器驱动并打开相应网页
    func.openUrl(url)

    # 登录
    func.login(cookiesFilePath)

    # 检索火车票
    func.searchTicket(row['trainInfo']['departure'], row['trainInfo']['destination'], row['trainInfo']['date'])

    # 滚动加载数据
    func.loadDataByScroll()

    # 检查坐席种类是否和配置文件一致
    func.checkSeats(data['seats'], data)

    # 进行筛选
    # 定位到预期车次并进入订单界面
    func.filterTickets(row['filter']['onlyAvailable'], row['filter']['trainTypes'], row['filter']['departTime'], row['filter']['arrivalTime'], data['seats'], row['trainInfo']['seatTypes'])

    # 滚动加载数据
    func.loadDataByScroll()

    # 添加乘客并预订进入待支付页面
    func.addPassengers(row['passengers'])

    # 等待
    sleep(3)

    # 关闭浏览器,释放资源
    func.close()

3. 测试层

在测试层中则是读取测试数据,并引入到逻辑层代码中实现自动化测试,具体代码如下:

# xc_book_test.py
# @Layered Architecture : 测试层
import pytest
from ruamel.yaml import YAML
from xctickets.xc_book import book_ticket
def readYaml():
    yaml = YAML()
    # 保留引号和注释
    yaml.preserve_quotes = True
    # 获取配置文件中的数据
    with open("./config.yaml", 'r', encoding='utf-8') as file:
        data = yaml.load(file)
    return data

data = readYaml()

param_data = [(data['cookiesFilePath'], row, data) for row in data['inputs']]
@pytest.mark.parametrize(["cookiesFilePath", "row", "data"], param_data)
def test_book_ticket(cookiesFilePath, row, data):
    try:
        book_ticket(cookiesFilePath, row, data)
    except Exception as E:
        print(E)

if __name__ == '__main__':
    pytest.main(["-s", "xc_book_test.py"])

4. 配置文件格式

# config.yaml
cookiesFilePath: "./cookies.txt"  # cookies本地存储路径

# 输入
inputs:
- trainInfo:
    departure: "开封"       # 出发站
    destination: "郑州"     # 到达站
    date: "2024-10-10"     # 日期
    seatTypes:
    - "硬座"
    - "二等座"
    - "商务座"
  passengers:
  - name: "张三"
    ID: "410201200010010125"
    tel: "13579325643"
    type: "成人"

  - name: "李四"
    ID: "410201200010010125"
    tel: "13579325644"
    type: "学生"
  filter:
    departTime: "15:00"     # 预期出发时间,最早
    arrivalTime: "18:00"    # 预期抵达时间,最晚
    onlyAvailable: true     # 仅显示有票车次
    trainTypes:   # 车型
    - type: "高铁(G/C)"
      select: false
    - type: "动车(D)"
      select: true
    - type: "普通(Z/T/K)"
      select: true
    - type: "其他(L/Y)"
      select: true
- trainInfo:
    departure: "郑州"       # 出发站
    destination: "开封"     # 到达站
    date: "2024-10-10"     # 日期
    seatTypes:
    - "硬座"
    - "二等座"
    - "商务座"
  passengers:
  - name: "张三"
    ID: "410201200010010125"
    tel: "13579325643"
    type: "成人"
  filter:
    departTime: "15:00"     # 预期出发时间,最早
    arrivalTime: "18:00"    # 预期抵达时间,最晚
    onlyAvailable: true     # 仅显示有票车次
    trainTypes:   # 车型
    - type: "高铁(G/C)"
      select: false
    - type: "动车(D)"
      select: true
    - type: "普通(Z/T/K)"
      select: true
    - type: "其他(L/Y)"
      select: true

seats:                 # 坐席
- type: "硬座"
  available: true
- type: "硬卧"
  available: true
- type: "软卧"
  available: true
- type: "无座"
  available: true
- type: "高级软卧"
  available: false
- type: "二等座"
  available: true
- type: "二等卧"
  available: true
- type: "一等卧"
  available: true
- type: "一等座"
  available: true
- type: "商务座"
  available: true
- type: "优选一等座"
  available: false

5. 配置文件相关解释

以下解释将基于配置文件config.yaml来进行。

  1. cookiesFilePath:用于存储cookies的txt文件存储地址,代码将检测本地该地址下是否存在名为cookies.txt的文件,如果存在并且其中有内容,则会自动向网站注入cookies实现自动登录;如果不存在或者其中没有内容,则会提醒你手动扫码登录并自动保存相应cookiescookies.txt文件中。
  2. inputs:用于存储多个输入内容的不同信息,其中包括:trainInfo火车信息、passengers乘客信息、filter火车票筛选。
  3. trainInfo:用于记录火车的出发站、到达站、日期以及预期的坐席种类(该坐席种类的先后顺序决定票类的优先级,最上面的优先级最高)
  4. passengers:用于记录乘客信息,其中包括:姓名、身份证号、手机号和乘客类型。该分类可以存储多个乘客信息。
  5. filter:用于记录筛选信息,其中包括:预期出发时间、预期到达时间、是否只显示有票车次、车型。这些信息将用于筛选出目标车次。
  6. seats:用于记录固定日期下,从出发站到到达站的所有坐席种类,以便于用户根据该信息进行坐席种类的选择,每次执行代码都会根据执行时的出发站、到达站和日期对坐席种类进行刷新。

侵权必删声明
本资料部分内容来源于互联网及公开渠道,仅供学习和交流使用,版权归原作者所有。若涉及版权问题,敬请原作者联系我,我将立即处理或删除相关内容。感谢您的理解与支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值