Web自动化之数据驱动(案例一、案例二)、JSON文件读与写、语法规则


今日内容

  • 数据驱动
    • 参数化
  • 日志模块
    • 基本应用
    • 高级应用

1. JSON 文件定义

  • JS 语言对象表示法. 是一种基于文本, 独立与语言, 轻量级的数据交换格式
    • Web 项目数据交换格式多为: JSON 或 XML
    • 移动端项目数据交换格式多为: JSON

2. JSON 文件语法规则

  • 最外层可以是对象{}或者是数组[]

  • 所有的数据必须是键值对(key-value)形式

    • 所有的 key 必须是字符串类型
    • 必须使用双引号
  • 多条数据由逗点隔开

  • 值的数据类型

    • 字符串
    • 数字类型
      • 整数
      • 浮点数
    • 布尔类型(全小写)
      • true
      • false
    • 空对象
      • null
  • 对象和数组可以相互嵌套

  • JSON演示

    {
      "name": "小明",
      "age": 18,
      "height": 1.85,
      "isMan": true,
      "school": null,
      "num": [1,2,3,4,5],
      "link": [
        {"BaiDu": "http://www.baidu.com"},
        {"黑马": "www.itheima.com"}
      ]
    }
    

3. JSON 文件的读写操作

无论是读取还是写入 JSON 文件, 都要使用 import json

  • 读取 JSON(重点)

    import json
    
    with open('./文件名.json', encoding='utf-8') as f:
        data = json.load(f)
        print('获取的 JSON 数据为:', data)
    
    • 读取的方法名: load()
    • with open() 方法, 默认为 r 模式, 可以省略
    • 读取的 JSON 文件内容会被自动转换为 Python 的字典数据(判断依据:布尔类型和空对象的语法格式)
  • 写入 JSON 文件

    import json
    
    dict_data = {"name": "小明", "isMan": False, "address": None}
    with open('./json_data.json', 'w', encoding='utf-8') as f:
      json.dump(dict_data, f)
    
    • 写入的方法名: dump()
    • 在写入 JSON 文件时, with open() 方法, 需要指定为 w 模式
    • 在写入 JSON 文件时, 中文会被自动转换为 unicode 编码(\u5c0f\u660e), 并不是乱码, 需要注意

4. 数据驱动案例一实现步骤

  • PO 对页面元素进行封装

    • 代码演示1–公共方法类
    """
    公共方法类
    """
    import time
    
    from selenium import webdriver
    
    
    class DriverUtil(object):
        """浏览器驱动工具类"""
        driver = None  # 初始化状态
    
        @classmethod
        def get_driver(cls):
            """获取驱动方法"""
            # 判断浏览器对象不存在时, 创建对象
            if cls.driver is None:
                cls.driver = webdriver.Chrome()  # 直接使用时需要修改此项(浏览器类型)
                cls.driver.get('http://cal.apple886.com/')  # 直接使用时需要修改此项(项目地址)
                cls.driver.maximize_window()
                cls.driver.implicitly_wait(10)
            return cls.driver
    
        @classmethod
        def quit_driver(cls):
            """退出驱动方法"""
            # 判断浏览器对象存在时, 再执行退出
            if cls.driver:
                cls.driver.quit()
                cls.driver = None
    
    
    if __name__ == '__main__':
        DriverUtil.get_driver()
        time.sleep(2)
        DriverUtil.quit_driver()
    
    • 代码演示2–计算器页面
    """
    计算器页面
    """
    from selenium.webdriver.common.by import By
    
    from utils import DriverUtil
    
    
    class CalcPage(object):
        """计算器-对象库层"""
    
        def __init__(self):
            self.driver = DriverUtil.get_driver()  # 获取浏览器驱动对象
    
            # self.num_btn = (By.ID, 'simple0')  # 数字键
            self.num_btn = (By.ID, 'simple{}')  # 数字键
            self.add_btn = (By.ID, 'simpleAdd')  # 加号键
            self.equal_btn = (By.ID, 'simpleEqual')  # 等号键
            self.result = (By.ID, 'resultIpt')  # 计算结果
    
        def find_num_btn(self, num):
            """数字键定位方法"""
            # Python 规范要求: 单行字符数量不超过 79 个(提高代码的可阅读性)
            # location = (self.num_btn[0], self.num_btn[1].format(num))
            # return self.driver.find_element(location[0], location[1])
            return self.driver.find_element(self.num_btn[0], self.num_btn[1].format(num))
    
        def find_add_btn(self):
            """加号键定位方法"""
            return self.driver.find_element(self.add_btn[0], self.add_btn[1])
    
        def find_equal_btn(self):
            """等号键定位方法"""
            return self.driver.find_element(self.equal_btn[0], self.equal_btn[1])
    
        def find_result(self):
            """计算结果定位方法"""
            return self.driver.find_element(self.result[0], self.result[1])
    
    
    class CalcHandle(object):
        """计算器-操作层"""
    
        def __init__(self):
            self.calc_page = CalcPage()  # 元素定位对象
    
        def click_num_btn(self, num):
            """数字键点击方法"""
            self.calc_page.find_num_btn(num).click()
    
        def click_add_btn(self):
            """加号键点击方法"""
            self.calc_page.find_add_btn().click()
    
        def click_equal_btn(self):
            """等号键点击方法"""
            self.calc_page.find_equal_btn().click()
    
        def get_result(self):
            """计算结果获取方法"""
            # 如果目标元素没有对应文本信息在代码中显示, 应该使用目标元素的 value 属性的属性值
            return self.calc_page.find_result().get_attribute('value')
    
        def input_number(self, num):
            """数字输入方法"""
            for i in str(num):
                self.click_num_btn(i)
    
    
    class CalcProxy(object):
        """计算器-业务层"""
    
        def __init__(self):
            self.calc_handle = CalcHandle()  # 元素操作对象
    
        def add_func(self, num1, num2):
            """加法运算方法"""
            # 通过循环遍历实现一次输入一个数字的效果
            # 注意: 整数类型数据不能够被遍历, 因此需要强转成字符串类型数据
            # for i in str(num1):
            #     self.calc_handle.click_num_btn(i)
    
            self.calc_handle.input_number(num1)  # 加数
            self.calc_handle.click_add_btn()  # 加号
            # for j in str(num2):
            #     self.calc_handle.click_num_btn(j)
    
            self.calc_handle.input_number(num2)  # 被加数
            self.calc_handle.click_equal_btn()  # 等号
    
        # 考虑到将来可能还需要增加减法/乘法/除法运算, 因此最好将获取计算结果方法抽取成单个方法
        def get_result_func(self):
            """获取计算结果方法"""
            return self.calc_handle.get_result()
    
  • 编写对应的测试用例

    • 代码演示–计算器加法测试用例
    """
    计算器加法测试用例
    """
    import json
    import unittest
    
    from calc_page import CalcProxy
    from utils import DriverUtil
    from parameterized import parameterized
    
    
    def build_add_data():
        with open('./add_data.json', encoding='utf-8') as f:
            data = json.load(f)
            print(data)
            return data
    
    
    class TestCalcAdd(unittest.TestCase):
        """加法测试类"""
    
        @classmethod
        def setUpClass(cls) -> None:
            cls.driver = DriverUtil.get_driver()  # 获取浏览器对象
            cls.calc_proxy = CalcProxy()  # 计算器页面业务执行对象
    
        @classmethod
        def tearDownClass(cls) -> None:
            DriverUtil.quit_driver()  # 退出浏览器对象
    
        @parameterized.expand(build_add_data())
        def test_add(self, x, y, z):
            """加法测试方法"""
            self.calc_proxy.add_func(x, y)  # 加法运算
            result = self.calc_proxy.get_result_func()  # 获取计算结果
            # 由于计算结果返回数值为字符串类型, 因此预期结果也应该为字符串类型.
            self.assertEqual(str(z), result)  # 断言判断
    
        # def test_add(self):
        #     """加法测试方法"""
        #     self.calc_proxy.add_func(110, 119)  # 加法运算
        #     result = self.calc_proxy.get_result_func()  # 获取计算结果
        #     # 由于计算结果返回数值为字符串类型, 因此预期结果也应该为字符串类型.
        #     self.assertEqual('229', result)  # 断言判断
    
    
    if __name__ == '__main__':
        unittest.main()
    
  • 准备数据源文件(JSON 文件)

    • JSON文件
    [
      [1,2,3],
      [12,13,25],
      [110, 120, 230],
      [1111,2222,3333]
    ]
    
  • 编写解析数据源文件的方法

    • 代码演示–解析数据源文件方法
    import json
    
    def build_add_data():
        with open('../add_data.json', encoding='utf-8') as f:
            data = json.load(f)
            print(data)
            return data
    
  • 利用参数化将数据源文件的中数据引入到测试用例的测试方法中

    • 已引入上述测试用例中

5. 数据驱动案例二-实现步骤

  • 公共方法类

    • utils.py
    • 代码演示
    from selenium import webdriver
    
    
    def get_tip_message():
        """获取弹窗信息方法"""
        msg = DriverUtil.get_driver().find_element_by_class_name('layui-layer-content').text
        print('msg:', msg)
        return msg
    
    
    class DriverUtil(object):
        """浏览器驱动对象工具类"""
    
        driver = None  # 驱动对象初始化状态
    
        @classmethod
        def get_driver(cls):
            """获取驱动对象方法"""
            # 判断浏览器对象不存在时再进行创建操作
            if cls.driver is None:
                cls.driver = webdriver.Chrome()
                # 由于判断条件下的代码只会执行一次, 因此将打开和最大化和隐式等待的设置暂时放置到这里
                cls.driver.get('http://127.0.0.1')
                cls.driver.maximize_window()
                cls.driver.implicitly_wait(10)
            return cls.driver
    
        @classmethod
        def quit_driver(cls):
            """退出驱动对象方法"""
            # 判断驱动对象存在时再执行退出操作
            if cls.driver:
                cls.driver.quit()
                cls.driver = None
    
    
    if __name__ == '__main__':
        DriverUtil.get_driver()
        DriverUtil.quit_driver()
    
  • PO 文件基类

    • base_page.py
    • 代码演示
    """
    PO 文件基类
    """
    from utils import DriverUtil
    
    
    class BasePage(object):
        """对象库层基类"""
    
        def __init__(self):
            self.driver = DriverUtil.get_driver()  # 获取浏览器对象
    
        def find_element_func(self, location):
            """元素定位方法"""
            return self.driver.find_element(location[0], location[1])
    
    
    class BaseHandle(object):
        """操作层基类"""
    
        @staticmethod
        def input_text(element, text):
            """元素输入方法"""
            element.clear()  # 清空
            element.send_keys(text)  # 输入
    
  • 对应页面的 PO 代码封装实现

    • 代码演示–登录页面
    """
    登录页面
    """
    from selenium.webdriver.common.by import By
    from base_page import BasePage, BaseHandle
    
    
    class LoginPage(BasePage):
        """对象库层(封装元素定位方法)"""
    
        def __init__(self):
            super().__init__()  # 获取父类浏览器对象
    
            self.username = (By.ID, 'username')  # 用户名
            self.password = (By.ID, 'password')  # 密码
            self.code = (By.ID, 'verify_code')  # 验证码
            self.login_btn = (By.NAME, 'sbtbutton')  # 登录按钮
    
        def find_username(self):
            """用户名定位方法"""
            return self.find_element_func(self.username)
    
        def find_password(self):
            """密码定位方法"""
            return self.find_element_func(self.password)
    
        def find_code(self):
            """验证码定位方法"""
            return self.find_element_func(self.code)
    
        def find_login_btn(self):
            """登录按钮定位方法"""
            return self.find_element_func(self.login_btn)
    
    
    class LoginHandle(BaseHandle):
        """操作层(封装元素操作方法)"""
    
        def __init__(self):
            self.login_page = LoginPage()  # 元素对象定位类对象
    
        def input_username(self, name):
            """用户名输入方法"""
    
            self.input_text(self.login_page.find_username(), name)
    
        def input_password(self, pwd):
            """密码输入方法"""
    
            self.input_text(self.login_page.find_password(), pwd)
    
        def input_code(self, code):
            """验证码输入方法"""
    
            self.input_text(self.login_page.find_code(), code)
    
        def click_login_btn(self):
            """登录按钮点击方法"""
            self.login_page.find_login_btn().click()
    
    
    class LoginProxy(object):
        """业务层(封装具体的测试执行步骤)"""
    
        def __init__(self):
            self.login_handle = LoginHandle()  # 元素操作类对象
    
        def login_func(self, name, pwd, code):
            """登录方法"""
            self.login_handle.input_username(name)  # 输入用户名
            self.login_handle.input_password(pwd)  # 输入密码
            self.login_handle.input_code(code)  # 输入验证码
            self.login_handle.click_login_btn()  # 点击登录按钮
    
  • 测试用例的编写

    • 代码演示–测试用例
    """
    登录测试用例
    """
    import json
    import unittest
    import time
    import logging
    
    from utils import DriverUtil, get_tip_message
    from loign_page import LoginProxy
    from parameterized import parameterized
    
    logging.basicConfig(level=logging.DEBUG)
    
    
    def build_login_data():
        """登录数据构造方法"""
        with open('./login_data.json', encoding='utf-8') as f:
            data = json.load(f)
            data_list = list()  # Python 规范要求, 应该使用 list 类实例化一个空列表
            for i in data:
                data_list.append((i.get('username'),
                                  i.get('password'),
                                  i.get('code'),
                                  i.get('is_success'),
                                  i.get('expect')))
            print(data_list)
            logging.info(data_list)
            return data_list
    
    
    class TestTPShopLogin(unittest.TestCase):
        """登录测试类"""
    
        @classmethod
        def setUpClass(cls) -> None:
            cls.driver = DriverUtil.get_driver()  # 获取浏览器对象
            cls.login_proxy = LoginProxy()  # 登录页面业务执行对象
    
        @classmethod
        def tearDownClass(cls) -> None:
            time.sleep(3)
            DriverUtil.quit_driver()  # 退出浏览器对象
    
        def setUp(self) -> None:
            self.driver.get('http://127.0.0.1/')  # 打开首页
            # 点击首页的‘登录’链接,进入登录页面
            self.driver.find_element_by_link_text('登录').click()
    
        @parameterized.expand(build_login_data())
        def test_login_func(self, username, pwd, code, is_success, expect):
            """登录测试方法"""
    
            self.login_proxy.login_func(username, pwd, code)
    
            # 条件判断正向用例还是反向用例
            if is_success:
                # 正向用例断言
                time.sleep(2)
                title = self.driver.title  # 获取页面标题
                self.assertIn(expect, title)  # 断言判断
            else:
                # 反向用例断言
                time.sleep(2)
                # 获取错误提示信息
                msg = get_tip_message()
    
                # 设置断言判断测试结果
                self.assertIn(expect, msg)
    
        # def test_account_does_not_exist(self):
        #     """用户不存在"""
        #
        #     self.login_proxy.login_func('13811110000', '123456', '8888')
        #
        #     time.sleep(2)
        #     # 获取错误提示信息
        #     msg = get_tip_message()
        #
        #     # 设置断言判断测试结果
        #     self.assertIn('账号不存在', msg)
        #
        # def test_wrong_password(self):
        #     """密码错误"""
        #
        #     self.login_proxy.login_func('13800001111', 'error', '8888')
        #
        #     time.sleep(2)
        #     # 获取错误提示信息
        #     msg = get_tip_message()
        #
        #     # 设置断言判断测试结果
        #     self.assertIn('密码错误', msg)
    
    
    if __name__ == '__main__':
        unittest.main()
    
  • 准备数据源文件(JSON 文件)

    • JSON文件
    [
      {
        "desc": "用户名为空",
        "username": "",
        "password": "123456",
        "code": "8888",
        "is_success": false,
        "expect": "用户名不能为空"
      },
      {
        "desc": "密码为空",
        "username": "13800001111",
        "password": "",
        "code": "8888",
        "is_success": false,
        "expect": "密码不能为空"
      },
      {
        "desc": "密码错误",
        "username": "13800001111",
        "password": "error",
        "code": "8888",
        "is_success": false,
        "expect": "密码错误"
      },
      {
        "desc": "登录成功",
        "username": "13800001111",
        "password": "123456",
        "code": "8888",
        "is_success": true,
        "expect": "我的账户"
      }
    ]
    
  • 编写解析数据源文件的方法

    • 代码演示–解析数据源文件方法
    import json
    
    def build_login_data():
        """登录数据构造方法"""
        with open('./login_data.json', encoding='utf-8') as f:
            data = json.load(f)
            data_list = list()  # Python 规范要求, 应该使用 list 类实例化一个空列表
            for i in data:
                data_list.append((i.get('username'),
                                  i.get('password'),
                                  i.get('code'),
                                  i.get('is_success'),
                                  i.get('expect')))
            print(data_list)
            return data_list
    
  • 利用参数化将数据源文件的中数据引入到测试用例的测试方法中

    • 已引入上述测试用例中

数据驱动

  • 定义:

    在测试代码逻辑不变的前提下, 通过控制测试数据来决定测试结果的过程

  • 特点:

    • 不是工业级标准概念, 不同公司会有不同看法
    • 有人认为属于一种模式, 还有人认为只是一种测试思想
    • 数据驱动的实现要依赖参数化技术
  • 数据来源:

    • 直接写死在测试代码中
    • 数据源文件
      • JSON
      • excel
      • XML
      • txt
    • 数据库
    • 能够获取数据接口
    • 本地构造数据的方法


版权声明:本文为limy_liu原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。