自动化测试当中的三大设计技巧:PO设计思想,数据驱动及关键字驱动

时间:2022-07-07 01:23:07


大家好,我是洋子。当我们以离线脚本的形式编写了大量的自动化测试代码后,很容易发现以下常见问题:

(1)对于UI自动化,当UI层的元素发生改变,需要修改所有相关的case,工作量巨大
(2)代码难以扩展,每次想新增一个自动化case就要写新的逻辑,补充新的代码
(3)代码可读性差,代码冗余,存在大量重复代码或者逻辑相似性代码

对于问题(1),可以引入POM设计思想解决
对于问题(2),可以引入数据驱动解决
对于问题(3),可以引入关键字驱动解决

这三种设计技巧分别是什么意思,怎么使用,我们一起来一探究竟

什么是POM设计思想

POM(Page Object Model)指的是页面对象模型,是一种设计思想(网上的大量文章会称为它是一种设计模式,学过开发的小伙伴可能有疑惑,设计模式通常指单例模式工厂模式等,没听过哪里还有什么POM设计模式,为了避免误解,在本文我还是将POM称为一种设计思想)

POM设计思想将页面UI元素对象业务逻辑(定位元素 以及 操作定位后的元素)Case测试数据等分离开来,使得代码逻辑更加清晰,复用性,可维护性更高的一种方法,普遍运用于UI自动化测试当中

通过一个小Demo来感受一下POM设计思想的好处,下面这个Demo基于unittest单元测试框架

unittest是python自带的一个单元测试框架,不仅适用于单元测试,还可用于UI自动化、接口自动化测试用例的开发与执行;此框架可以组织执行测试用例,并且提供了丰富的断言方法,判断测试用例是否执行通过,并生成测试结果

在测试类TestSeach当中定义了testSeachPage方法作为一个UI自动化case,这个case主要验证百度主页的搜索功能,其操作步骤为打开百度官网,定位到页面当中的搜索框元素,输入文字,再定位到页面当中的搜索按钮元素,进行点击按钮操作搜索,最后进行搜索结果断言

from selenium import webdriver
import time
import unittest


class TestSeach(unittest.TestCase):

    def testSeachPage(self):
        
        # ======测试基础数据===================
        url = "http://www.baidu.com"
        # 搜索输入框
        search_input = {"id": "kw"}
        # 百度一下 按钮
        search_button = {"id": "su"}
        # ======测试基础数据===================

        # ======定位元素,操作元素以及断言=================================

        # 启动WebDriver,地址填写本地下载的WebDriver的路径
        driver = webdriver.Chrome("/Users/yangzi/Downloads/chromedriver")

        # 访问百度
        driver.get(url)
        # 定位元素,并进行相应操作
        driver.find_element("id", search_input["id"]).send_keys("测试开发学习路线通关大厂")
        driver.find_element("id", search_button["id"]).click()

        # 断言 (仅演示,下面的语句断言结果永为真) 
        # assertEqual()为unittest内置方法
        self.assertEqual(url, "http://www.baidu.com", msg="input url error!")

        # 睡眠5秒
        time.sleep(5)
        # 释放资源, 退出浏览器
        driver.quit()

        # ======定位元素,操作元素以及断言 ============================


if __name__ == '__main__':
    unittest.main()

在以上的Demo当中,我们把写自动化case所必须的URL,元素id的值等必要的测试case基础数据定位以及操作元素的方法,以及断言方法都写在了TestSeach测试类当中的testSeachPage方法,testSeachPage方法就充当了一个UI自动化case,但是在这个case当中各部分代码耦合在一起

单独看这段Demo代码感觉还非常简单,这是因为我们只访问了1个页面,并且定位元素以及对元素操作的次数非常少,维护这样一个简单的UI页面自动化case还非常容易

在实际情况下,我们往往要维护几十个甚至上百个页面,假设某个页面新增或者删除了一个元素,若有100个case用到了该页面,在最坏情况下,我们都得依次手动修改这100个case当中的代码逻辑,这样会让我们的脚本维护变得繁琐复杂,而且变得耗时易出错

虽说IDE有相关功能可以一键修改,但采用良好的设计思想能大大提高我们的case维护效率

总的来说,与其用一个类"充胖子",不如拆分成3个类各司其职,这三个类分别是基类、某个具体的页面类(继承基类)、测试类

自动化测试当中的三大设计技巧:PO设计思想,数据驱动及关键字驱动


接下来我们利用POM设计思想,对上面的Demo进行改写

首先定义一个基类Base Page,这是所有页面的父类。在这个基类当中定义了定位、操作定位后元素的基础方法,保证能够提供对元素进行点击赋值获取值等的能力

# page基类
class Page(object):
    """
        Page基类,所有页面page都应该继承该类
    """

    def __init__(self, driver, base_url="https://www.baidu.com"):
        self.driver = driver
        self.base_url = base_url

    def find_element(self, *loc):
        return self.driver.find_element(*loc)

    def get_attribute(self, attribute, loc):
        return self.find_element(*loc).get_attribute(attribute)

    def input_text(self, loc, text):
        self.find_element(*loc).send_keys(text)

    def click(self, loc):
        self.find_element(*loc).click()

接着定义某个具体的页面类(为方便说明,以下统称页面类A),页面类A继承了基类Base Page,在具体的页面类当中,可以重写或者二次封装基类当中定义的方法,使其更加易读,具备更加定制化的功能

from selenium.webdriver.common.by import By
from BasePage import Page


# 百度搜索page
class SearchPage(Page):
    # 百度搜索页面的元素信息(定位元素的方式,以及对应的值)

    # 搜索输入框 元素
    search_input = (By.ID, 'kw')
    # 百度一下按钮 元素
    search_button = (By.ID, 'su')
    # 百度热搜 元素
    hot_search = (By.XPATH, '//*[@]/div/div/div[1]')

    def __init__(self, driver, base_url="https://www.baidu.com"):
        Page.__init__(self, driver, base_url)

    def openBaiduHomePage(self):
        print("打开首页: ", self.base_url)
        self.driver.get(self.base_url)

    def input_search_text(self, text="testerGuie"):
        print("输入搜索关键字:测试开发Guide")
        self.input_text(self.search_input, text)

    def click_search_btn(self):
        print("点击 百度一下  按钮")
        self.click(self.search_button)

    def get_hot_search_title(self):
        return self.get_attribute("title",self.hot_search)

最后就是测试类,在测试类当中创建 页面类A 的对象,调用二次封装的方法当中填入测试数据,并添加断言,从而构成了整个测试case

import unittest
from selenium import webdriver
from SearchPage import SearchPage


# 百度搜索测试
class TestSearchPage(unittest.TestCase):

    def setUp(self):
    	# webdriver的path请修改为自己本地路径
        self.driver = webdriver.Chrome("/Users/yangzi/Downloads/chromedriver")

    def testSearch(self):
        driver = self.driver
        driver.implicitly_wait(10)
        
        # 百度网址
        url = "https://www.baidu.com"
        # 搜索文本
        text = "测试开发进大厂"

        # 断言,期望验证的标题
        assert_title = "百度热搜"

        search_Page = SearchPage(driver, url)

        # 启动浏览器,访问百度首页
        search_Page.openBaiduHomePage()
        
        # 输入 搜索词
        search_Page.input_search_text(text)

        # 单击 百度一下 按钮进行搜索
        search_Page.click_search_btn()

        # 验证标题
        self.assertEqual(search_Page.get_hot_search_title(), assert_title)

    def tearDown(self):
        self.driver.quit()


if __name__ == '__main__':
    unittest.main()

经过采用POM设计,相比最开始的Demo,看上去结构似乎没有多大变化,反而增加定义了额外的类,确实因为举例非常简单,但当测试case逐步增多,你会发现这样设计的好处

另外,这个测试用例还有一些能够优化的地方,比如说测试数据像网页的url、搜索的文本text、断言的期望结果等测试数据都是写死在代码当中,每新增一个case,就要在代码当中编写新的数据,我们有没有办法填写新的case时不再修改代码,减少代码冗余,只补充测试数据即可,数据驱动设计思想就能达成此目的

什么是数据驱动

本篇文章提到的数据驱动,指在软件测试领域当中的数据驱动测试(Data-Driven Testing,简称DDT)是一种软件测试方法,在数据源的帮助下重复执行相同顺序的测试步骤,测试脚本从数据源读取测试数据,而不使用硬编码值(测试数据写死在代码中)

数据源通常有以下情况:

  1. 数据硬编码在自动化测试脚本中
  2. 文件读取数据,如JsonExcelCSVYaml等格式文件
  3. 数据库中读取数据
  4. 调用接口获取数据源
  5. 本地编写数据生成的方法(脚本)

对于第1种,直接把测试数据定义在脚本中,虽然简单直观,但没有做到“测试数据”与“执行代码”解耦,不方便后期维护

对于3,4,5种情况,在自动化测试当中也会使用到,如部分参数构造,测试脚本获取其他外部数据用于脚本当中的业务逻辑等场景

在自动化测试的离线脚本当中,对于人工编写自动化case的场景,我们通常采用第2种情况,在文件当中定义自动化case的测试数据,测试脚本从文件读取数据执行自动化测试

文件读写在各大编程语言当中都提供丰富的方法支持,下面用Python举例,分别读取JsonExcelCSVYaml文件的内容,工程目录见下图

自动化测试当中的三大设计技巧:PO设计思想,数据驱动及关键字驱动

(1)读取Json文件内容

import json

def readJSON(filename):

    with open(file=filename, mode="r", encoding="UTF-8") as f:
        res=json.load(f)
    return res

if __name__ == '__main__':

    searchPageTestData = readJSON("../Case/searchPage.json")
    print(searchPageTestData["search"]["search_url"],searchPageTestData["search"]["search_text"],searchPageTestData["search"]["assert_title"])

(2)读取Excel文件内容

import xlrd


# 读取行
def readExcelByRow():
    lists = []
    book = xlrd.open_workbook("../Case/searchPage.xls")  # 打开Excel文件,存在book里(不用一定叫book,改成别的名字也可以,但是要记得下面一行的book也要替换)
    sheet = book.sheet_by_index(0)  # 索引是0的是第一张表
    for item in range(1, sheet.nrows):  # 按行读取,for循环就是输出所要求的行
        lists.append(sheet.row_values(item))  # 转换为表格形式
    return lists


# 读取列
def readExcelByCol():
    lists = []
    book = xlrd.open_workbook("../Case/searchPage.xls")
    sheet = book.sheet_by_index(0)
    for item in range(1, sheet.ncols):  # 按列读取
        lists.append(sheet.col_values(item))
    return lists


if __name__ == '__main__':
    rows = readExcelByRow()
    cols = readExcelByCol()

    print("Excel文件按行遍历 rows", rows)
    print("Excel文件按列遍历 cols", cols)

(3)读取CSV文件内容

import csv


def readCsvList():
    lists = []  # 新建一个列表,将要提取的数据放到这个新列表里
    with open(file="../Case/searchPage.csv", mode="r", encoding="UTF-8") as f:
        reader = csv.reader(f)  # 读取列表样式.reader()
        next(reader)            # 不读取第一行casename,search_url,search_text,assert_title
        for item in reader:
            lists.append(item)
    return lists


if __name__ == '__main__':
    res = readCsvList()
    print(res)

(4)读取Yaml文件内容

import yaml


def readYaml():
    with open(file="../Case/searchPage.yaml", mode="r", encoding="UTF-8") as f:
        return yaml.safe_load(f)


if __name__ == '__main__':

    searchPage = readYaml()

    print("返回数据类型", type(readYaml()))
    print(searchPage["search"]["search_url"])
    print(searchPage["search"]["search_text"])
    print(searchPage["search"]["assert_title"])

最后以读取Json文件为例,改写测试类的内容,测试数据url、搜索文本text、断言的标题均从Json文件当中获取

import unittest
from selenium import webdriver
from POM.SearchPage import SearchPage

from Utils.readJson import readJSON

# 百度搜索测试

class TestSearchPage(unittest.TestCase):

    def setUp(self):
        self.driver = webdriver.Chrome("/Users/yangzi/Downloads/chromedriver")

    def testSearch(self):
        driver = self.driver
        driver.implicitly_wait(10)
		
		# 从Json文件获取测试case数据
        searchPageTestData = readJSON("../Case/searchPage.json")

        # 百度网址
        url = searchPageTestData["search"]["search_url"]
        # 搜索文本
        text = searchPageTestData["search"]["search_text"]

        # 断言,期望验证的标题
        assert_title = searchPageTestData["search"]["assert_title"]

        search_Page = SearchPage(driver, url)

        # 启动浏览器,访问百度首页
        search_Page.openBaiduHomePage()

        # 输入 搜索词
        search_Page.input_search_text(text)

        # 单击 百度一下 按钮进行搜索
        search_Page.click_search_btn()

        # 验证标题
        self.assertEqual(search_Page.get_hot_search_title(), assert_title)

    def tearDown(self):
        self.driver.quit()


if __name__ == '__main__':
    unittest.main()

采用数据驱动思想后,后续新增case只需要在Json文件补充测试数据即可,其他文件格式同理

{
  "search": {"search_url": "https://www.baidu.com" ,"search_text": "测试开发进大厂","assert_title":"百度热搜"},
  "search_02": {"search_url": "xxxx" ,"search_text": "xxxx","assert_title":"xxxx"},
  "search_03": {"search_url": "yyyy" ,"search_text": "yyy","assert_title":"yyyy"}
}

目前部分自动化测试框架也自带了的数据驱动功能:

  • Unitest:使用装饰器@ddt 装饰测试类,@data或者@file_data来装饰需要驱动的测试方法,可实现数据驱动
  • Pytest:使用装饰器@pytest.mark.parametrize("xx")
  • TestNG:使用注解@DataProvider
  • Junit:使用注解@ParameterizedTest+@ValueSource

值得关注的一点,自动化测试框架自带的数据驱动功能,也是将测试数据写在代码当中,但通过装饰器或者注解的形式完成了代码和测试数据解耦,大家可以根据自己的情况灵活选择数据驱动的方式

什么是关键字驱动

在前面的POM设计,在具体的UI页面类当中,二次封装Base Page Class 基类当中的方法,使得更加易读,提现了封装的思想,而关键字驱动实质还是体现封装的思想

关键字驱动是指将所有case依赖的公共步骤,进行再次封装,形成关键字,调用不同的关键字组合实现不同的业务逻辑,从而驱动测试用例执行

关键字驱动的实现方法一般有两种:
(1)第一种:自己手动实现关键字,进行公共步骤的二次封装
(2)第二种:某些自动化测试框架已经自带关键字功能,可直接使用或者扩展自定义关键字

手动实现关键字

还是以上面搜索页面UI自动化测试Case为例,对于其他所有要用到百度页面搜索功能的case,都需要调用三个公共方法,分别是打开百度搜索openBaiduHomePage方法,输入搜索内容input_search_text(text)方法,点击搜索按钮click_search_btn()方法

可以将这三个公共方法,封装为一个关键字

自动化测试当中的三大设计技巧:PO设计思想,数据驱动及关键字驱动


经过关键字驱动改写后的测试case如下

import unittest
from selenium import webdriver
from POM.SearchPage import SearchPage

from Utils.readJson import readJSON

# 百度搜索测试



class TestSearchPage(unittest.TestCase):

    def setUp(self):
        self.driver = webdriver.Chrome("/Users/yangzi/Downloads/chromedriver")

    def testSearch(self):
        driver = self.driver
        driver.implicitly_wait(10)

        searchPageTestData = readJSON("../Case/searchPage.json")

        # 百度网址
        url = searchPageTestData["search"]["search_url"]
        # 搜索文本
        text = searchPageTestData["search"]["search_text"]

        # 断言,期望验证的标题
        assert_title = searchPageTestData["search"]["assert_title"]

        search_Page = SearchPage(driver, url)

        # 调用搜索关键字,完成搜索功能
        search_Page.search_keyword(text)

        # 验证标题
        self.assertEqual(search_Page.get_hot_search_title(), assert_title)

    def tearDown(self):
        self.driver.quit()


if __name__ == '__main__':
    unittest.main()

还可以进一步进行优化降低业务测试人员编写自动化测试case的门槛,将关键字驱动和数据驱动进行结合,把关键字定义在Excel或CSV外部文件当中,读取文件当中的关键字,利用反射机制执行关键字的方法,各大编程语言均支持反射机制,对于Java的反射,可以使用invoke方法执行关键字方法

感兴趣可以阅读文末推荐的文章《自动化测试关键字驱动的原理及实现》Java和Python实现版本

自动化测试框架自带关键字

Robot Framework是基于关键字驱动的自动化测试框架,RF框架本身利用Python实现,已集成定义关键字功能

在引入RF框架的robot依赖库后的内部文件robot/api/deco.py可以看到 @keyword('key name')装饰器的定义,从注释中可以看到@keyword的使用举例,详细用法可以查看《Robot Framework自动化测试企业级实战教程》

def keyword(name=None, tags=(), types=()):
    """Decorator to set custom name, tags and argument types to keywords.

    This decorator creates ``robot_name``, ``robot_tags`` and ``robot_types``
    attributes on the decorated keyword function or method based on the
    provided arguments. Robot Framework checks them to determine the keyword's
    name, tags, and argument types, respectively.

    Name must be given as a string, tags as a list of strings, and types
    either as a dictionary mapping argument names to types or as a list
    of types mapped to arguments based on position. It is OK to specify types
    only to some arguments, and setting ``types`` to ``None`` disables type
    conversion altogether.

    If the automatic keyword discovery has been disabled with the
    :func:`library` decorator or by setting the ``ROBOT_AUTO_KEYWORDS``
    attribute to a false value, this decorator is needed to mark functions
    or methods keywords.

    Examples::

        @keyword
        def example():
            # ...

        @keyword('Login as user "${user}" with password "${password}"',
                 tags=['custom name', 'embedded arguments', 'tags'])
        def login(user, password):
            # ...

        @keyword(types={'length': int, 'case_insensitive': bool})
        def types_as_dict(length, case_insensitive):
            # ...

        @keyword(types=[int, bool])
        def types_as_list(length, case_insensitive):
            # ...

        @keyword(types=None])
        def no_conversion(length, case_insensitive=False):
            # ...
    """
    if inspect.isroutine(name):
        return keyword()(name)

    def decorator(func):
        func.robot_name = name
        func.robot_tags = tags
        func.robot_types = types
        return func

    return decorator

结束语

本文介绍了自动化测试当中常用的三大技巧,PO设计思想,数据驱动及关键字驱动,大家可以在实际工作当中单独使用某种设计,也可以将三者组合起来使用,减少在量级过大的自动化Case维护成本