前端测试 python 常用 UI 自动化设计模式总结

红客联盟 · 2018年11月15日 · 最后由 Ju-87C 回复于 2021年05月18日 · 812 次阅读

众所周知,UI 自动化是出了名的不稳定。由于测试代码能力弱的缘故,往往是开发改一行代码,测试改一千行代码,疲于奔命。因此遵循一些常用设计模式就很有必要。这里梳理总结一些 UI 测试常用设计模式,力求简单易懂,以及设计模式里对测试最有用的。以备查阅。这里参照社区大佬的两篇帖子学习总结,可以对照着看看

[1]: https://testerhome.com/topics/15540 "测试开发之路--UI 自动化设计军规"
[2]: https://testerhome.com/topics/15768 "测试开发之路--UI 自动化常用设计模式"

page object 设计模式

所有模块设计均遵循 page object 结构

  • 用例层:测试人员编写测试用例代码的地方,可以调用 page 层和封装层。
  • page 层:一个页面一个类,包含该页面的业务逻辑封装以及部分控件定义。
  • 封装层:根据业务需要,封装常用的业务逻辑 (相比于 page 层的业务逻辑封装,它的范围更广,有些时候是跨页面的业务逻辑。 属于模块级的业务封装)

工厂模式

1.简单工厂模式

#encoding=utf-8
__author__ = 'kevinlu1010@qq.com'


class LeiFeng():
    def buy_rice(self):
        pass

    def sweep(self):
        pass


class Student(LeiFeng):
    def buy_rice(self):
        print '大学生帮你买米'

    def sweep(self):
        print '大学生帮你扫地'


class Volunteer(LeiFeng):
    def buy_rice(self):
        print '社区志愿者帮你买米'

    def sweep(self):
        print '社区志愿者帮你扫地'


class LeiFengFactory():
    def create_lei_feng(self, type):
        map_ = {
            '大学生': Student(),
            '社区志愿者': Volunteer()
        }
        return map_[type]


if __name__ == '__main__':
    leifeng1 = LeiFengFactory().create_lei_feng('大学生')
    leifeng2 = LeiFengFactory().create_lei_feng('大学生')
    leifeng3 = LeiFengFactory().create_lei_feng('大学生')
    leifeng1.buy_rice()
    leifeng1.sweep()

写一个雷锋类,定义买米和扫地两个方法,写一个学生类和社区志愿者类,继承雷锋类,写一个工厂类,根据输入的类型返回学生类或志愿者类。

2.工厂模式

#encoding=utf-8
__author__ = 'kevinlu1010@qq.com'


class LeiFeng():
    def buy_rice(self):
        pass

    def sweep(self):
        pass


class Student(LeiFeng):
    def buy_rice(self):
        print '大学生帮你买米'

    def sweep(self):
        print '大学生帮你扫地'


class Volunteer(LeiFeng):
    def buy_rice(self):
        print '社区志愿者帮你买米'

    def sweep(self):
        print '社区志愿者帮你扫地'


class LeiFengFactory():
    def create_lei_feng(self):
        pass


class StudentFactory(LeiFengFactory):
    def create_lei_feng(self):
        return Student()


class VolunteerFactory(LeiFengFactory):
    def create_lei_feng(self):
        return Volunteer()


if __name__ == '__main__':
    myFactory = StudentFactory()

    leifeng1 = myFactory.create_lei_feng()
    leifeng2 = myFactory.create_lei_feng()
    leifeng3 = myFactory.create_lei_feng()

    leifeng1.buy_rice()
    leifeng1.sweep()

雷锋类,大学生类,志愿者类和简单工厂一样,新写一个工厂方法基类,定义一个工厂方法接口(工厂方法模式的工厂方法应该就是指这个方法),然后写一个学生工厂类,志愿者工厂类,重新工厂方法,返回各自的类。

工厂方法相对于简单工厂的优点:

1.在简单工厂中,如果需要新增类,例如加一个中学生类(MiddleStudent),就需要新写一个类,同时要修改工厂类的 map_,加入'中学生':MiddleStudent()。这样就违背了封闭开放原则中的一个类写好后,尽量不要修改里面的内容,这个原则。而在工厂方法中,需要增加一个中学生类和一个中学生工厂类(MiddleStudentFactory),虽然比较繁琐,但是符合封闭开放原则。在工厂方法中,将判断输入的类型,返回相应的类这个过程从工厂类中移到了客户端中实现,所以当需要新增类是,也是要修改代码的,不过是改客户端的代码而不是工厂类的代码。

2.对代码的修改会更加方便。例如在客户端中,需要将 Student 的实现改为 Volunteer,如果在简单工厂中,就需要把

leifeng1 = LeiFengFactory().create_lei_feng('大学生')

中的大学生改成社区志愿者,这里就需要改三处地方,但是在工厂方法中,只需要吧

myFactory = StudentFactory()

改成

myFactory = VolunteerFactory()

就可以了

单例模式

当我们实例化一个对象时,是先执行了类的 new 方法(我们没写时,默认调用 object.new),实例化对象;然后再执行类的 init 方法,对这个对象进行初始化,所以我们可以基于这个,实现单例模式

class Earth(object):    
    __instance=None #定义一个类属性做判断     
    def __new__(cls):         
        if cls.__instance==None:            
            #如果__instance为空证明是第一次创建实例            
            #通过父类的__new__(cls)创建实例                                             
            cls.__instance==object.__new__(cls)            
            return  cls.__instance        
        else:            
            #返回上一个对象的引用            
            return cls.__instance 
a = Earth()
print(id(a))
b = Earth()
print(id(b))

模板模式

模板方法模式时行为模式中比较简单的设计模式之一。模板方法关注这样的一类行为:该类行为在执行过程中拥有大致相同的动作次序,只是动作在实现的具体细节上有所差异

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类不改变一个算法的结构即可重定义该算法的某些特定步骤。

实例

投资股票是种常见的理财方式,我国股民越来越多,实时查询股票的需求也越来越大。今天,我们通过一个简单的股票查询客户端来认识一种简单的设计模式:模板模式。 根据股票代码来查询股价分为如下几个步骤:登录、设置股票代码、查询、展示。

#构造如下的虚拟股票查询器:
class StockQueryDevice():
    stock_code="0"
    stock_price=0.0
    def login(self,usr,pwd):
        pass
    def setCode(self,code):
        self.stock_code=code
    def queryPrice(self):
        pass
    def showPrice(self):
        pass

#根据不同的查询机构和方式来通过继承的方式实现其的股票查询器类。
#WebA和WebB的查询器类可以构造如下:
class WebAStockQueryDevice(StockQueryDevice):
    def login(self,usr,pwd):
        if usr=="myStockA" and pwd=="myPwdA":
            print "Web A:Login OK... user:%s pwd:%s"%(usr,pwd)
            return True
        else:
            print "Web A:Login ERROR... user:%s pwd:%s"%(usr,pwd)
            return False
    def queryPrice(self):
        print "Web A Querying...code:%s "%self.stock_code
        self.stock_price=20.00
    def showPrice(self):
        print "Web A Stock Price...code:%s price:%s"%(self.stock_code,self.stock_price)
class WebBStockQueryDevice(StockQueryDevice):
    def login(self,usr,pwd):
        if usr=="myStockB" and pwd=="myPwdB":
            print "Web B:Login OK... user:%s pwd:%s"%(usr,pwd)
            return True
        else:
            print "Web B:Login ERROR... user:%s pwd:%s"%(usr,pwd)
            return False
    def queryPrice(self):
        print "Web B Querying...code:%s "%self.stock_code
        self.stock_price=30.00
    def showPrice(self):
        print "Web B Stock Price...code:%s price:%s"%(self.stock_code,self.stock_price)

#在场景中,想要在网站A上查询股票
if  __name__=="__main__":
    web_a_query_dev=WebAStockQueryDevice()
    web_a_query_dev.login("myStockA","myPwdA")
    web_a_query_dev.setCode("12345")
    web_a_query_dev.queryPrice()
    web_a_query_dev.showPrice()

打印结果:

Web A:Login OK... user:myStockA pwd:myPwdA
Web A Querying...code:12345
Web A Stock Price...code:12345 price:20.0

但是发现每次操作,都会调用登录,设置代码,查询,展示这几步,是不是有些繁琐?既然有些繁琐,何不将这几步过程封装成一个接口。由于各个子类中的操作过程基本满足这个流程,所以这个方法可以写在父类中

class StockQueryDevice():
    stock_code="0"
    stock_price=0.0
    def login(self,usr,pwd):
        pass
    def setCode(self,code):
        self.stock_code=code
    def queryPrice(self):
        pass
    def showPrice(self):
        pass

    def operateQuery(self, usr, pwd, code):
        if not self.login(usr, pwd):
            return False
        self.setCode(code)
        self.queryPrice()
        self.showPrice()
        return True

class WebAStockQueryDevice(StockQueryDevice):
    def login(self,usr,pwd):
        if usr=="myStockA" and pwd=="myPwdA":
            print("Web A:Login OK... user:%s pwd:%s"%(usr,pwd))
            return True
        else:
            print("Web A:Login ERROR... user:%s pwd:%s"%(usr,pwd))
            return False
    def queryPrice(self):
        print("Web A Querying...code:%s "%self.stock_code)
        self.stock_price=20.00
    def showPrice(self):
        print("Web A Stock Price...code:%s price:%s"%(self.stock_code,self.stock_price))
class WebBStockQueryDevice(StockQueryDevice):
    def login(self,usr,pwd):
        if usr=="myStockB" and pwd=="myPwdB":
            print("Web B:Login OK... user:%s pwd:%s"%(usr,pwd))
            return True
        else:
            print("Web B:Login ERROR... user:%s pwd:%s"%(usr,pwd))
            return False
    def queryPrice(self):
        print("Web B Querying...code:%s "%self.stock_code)
        self.stock_price=30.00
    def showPrice(self):
        print("Web B Stock Price...code:%s price:%s"%(self.stock_code,self.stock_price))


if  __name__=="__main__":
    web_a_query_dev=WebAStockQueryDevice()
    web_a_query_dev.operateQuery("myStockA","myPwdA","12345")

打印结果相同:

Web A:Login OK... user:myStockA pwd:myPwdA
Web A Querying...code:12345
Web A Stock Price...code:12345 price:20.0

模式优点

在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序 。提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为。 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行 更换和增加新的子类很方便,符合单一职责原则和开闭原则

模式缺点

需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统会更加庞大,设计也会更加抽象

策略模式

模拟鸭子应用问题作为实例,一款游戏应用中要求有各种各样的鸭子。

先用继承来实现了这一个应用,其设计如下:

这个设计主要是以 Duck 类作为基类,后面所有的鸭子类均以此类派生而来,刚开始在应用还不是很复杂的时候,似乎还没有什么问题,但当派生出来的类(鸭子的类型)越来越多时,问题就出现了。并不是所有的鸭子都会飞的,比如像皮鸭子就不会飞。也不是所有的鸭子都会呱呱叫,也有鸭子吱吱叫。也许你会说,我可以在派生类中重写基类的方法,从而达到应用的要求,实现不同的鸭子有不同的叫法,不同的飞行方式。但是有一个问题不能解决,重写函数并不应该改变原有函数的行为,比如 fly() 这个函数,不能飞的鸭子就不应该有 fly() 这个函数存在。而如果我们只是重写基类的 fly() 函数,看起来是不合适的。并且 quack() 函数用于发出 “呱呱叫”,而基类中定义了 quack() 就意味着所有的鸭子中都有 quck(),如果现在要求鸭子 “吱吱叫”,怎么办呢?在基类中添加一个"吱吱叫 “的函数?那这样又会影响到其它的子类。
如此看来,派生并不解决问题最好的办法,或者说不能只用派生来解问题。
分析一下,得到以下设计原则:

  • 针对接口编程,而不是实现编程

  • 分离应用中经常变化的部分

最终,我们分开了易于变化的部分,飞行行为和呱呱叫行为,设计出来的类图如下:

python 代码实现如下:

''' 
The first Design Pattern:     
    Strategy Pattern.
KeyNote:    
    Identify the aspects of your application that vary and separate them     
    from what stays the same.
''' 
class FlyBehavior:    
    ''' Interface class: FlyBehavior '''    
    def fly(self):        
        return 
class FlyWithWing(FlyBehavior):    
    def fly(self):      
        print 'I am flying with wings!' 
class FlyNoWay(FlyBehavior):    
    def fly(self):        
        print 'I cannot fly!' 
class QuackBehavior:    
    ''' Interface Behavior: QuackBehavior '''    
    def quack(self):        
        return 
class Quack(QuackBehavior):    
    def quack(self):        
        print 'Quack!' 
class Squeak(QuackBehavior):   
    def quack(self):        
        print 'Squeak' 
class MuteQuack(QuackBehavior):    
    def quack(self):        
        print 'MuteQuack' 
class Duck:    
        '''Base class: Duck. All ducks are inherent from this class'''   
        def __init__(self, flyParam, quackParam):        
            self.flyBehavior = flyParam        
            self.quackBehavior = quackParam    
        def performFly(self):        
            self.flyBehavior.fly()    
        def performQuack(self):        
             self.quackBehavior.quack()    
        def swim(self):        
             print 'All ducks can swim...'        
             return     
        def display(self):        
             return
class RedDuck(Duck):    
        def __init__(self, flyParam=FlyWithWing(), quackParam=MuteQuack()):                     Duck.__init__(self, flyParam, quackParam)    
        def display(self):        
             print 'I am a red duck!'        
             return 
class RubberDuck(Duck):    
    def __init__(self, flyParam=FlyNoWay(), quackParam=Quack()):                        Duck.__init__(self, flyParam, quackParam)    
    def display(self):        
        print 'I am a rubber duck!' 
duck = RedDuck()
duck.display()
duck.performFly()
duck.performQuack()

duck.swim() 
duck = RubberDuck()
duck.display()
duck.performFly()
duck.performQuack()
duck.swim()

建造者模式

需求,画人物,要求画一个人的头,左手,右手,左脚,右脚和身体,画一个瘦子,一个胖子

不使用设计模式

#encoding=utf-8
__author__ = 'kevinlu1010@qq.com'

if __name__=='__name__':
    print '画左手'
    print '画右手'
    print '画左脚'
    print '画右脚'
    print '画胖身体'

    print '画左手'
    print '画右手'
    print '画左脚'
    print '画右脚'
    print '画瘦身体'

这样写的缺点每画一个人,都要依次得画他的六个部位,这些部位有一些事可以重用的,所以调用起来会比较繁琐,而且客户调用的时候可能会忘记画其中的一个部位,所以容易出错。

使用建造者模式

#encoding=utf-8
__author__ = 'kevinlu1010@qq.com'
from abc import ABCMeta, abstractmethod


class Builder():
    __metaclass__ = ABCMeta

    @abstractmethod
    def draw_left_arm(self):
        pass

    @abstractmethod
    def draw_right_arm(self):
        pass

    @abstractmethod
    def draw_left_foot(self):
        pass

    @abstractmethod
    def draw_right_foot(self):
        pass

    @abstractmethod
    def draw_head(self):
        pass

    @abstractmethod
    def draw_body(self):
        pass


class Thin(Builder):
    def draw_left_arm(self):
        print '画左手'

    def draw_right_arm(self):
        print '画右手'

    def draw_left_foot(self):
        print '画左脚'

    def draw_right_foot(self):
        print '画右脚'

    def draw_head(self):
        print '画头'

    def draw_body(self):
        print '画瘦身体'


class Fat(Builder):
    def draw_left_arm(self):
        print '画左手'

    def draw_right_arm(self):
        print '画右手'

    def draw_left_foot(self):
        print '画左脚'

    def draw_right_foot(self):
        print '画右脚'

    def draw_head(self):
        print '画头'

    def draw_body(self):
        print '画胖身体'


class Director():
    def __init__(self, person):
        self.person=person

    def draw(self):
        self.person.draw_left_arm()
        self.person.draw_right_arm()
        self.person.draw_left_foot()
        self.person.draw_right_foot()
        self.person.draw_head()
        self.person.draw_body()


if __name__=='__main__':
    thin=Thin()
    fat=Fat()
    director_thin=Director(thin)
    director_thin.draw()
    director_fat=Director(fat)
    director_fat.draw()

建造一个抽象的类 Builder,定义画六个部位的方法,每画一种人,就新建一个继承 Builder 的类,这样新建的类就必须要实现 Builder 的所以方法,这里主要运用了抽象方法的特性,父类定义了几个抽象的方法,子类必须要实现这些方法,否则就报错,这里解决了会漏画一个部位的问题。建造一个指挥者类 Director,输入一个 Builder 类,定义一个 draw 的方法,把画这六个部位的方法调用都放在里面,这样调用起来就不会繁琐了。

所以建造者模式用于将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 17 条回复 时间 点赞

看不太懂。。

非常有用,正适合我这种新手

你说的这些设计模式跟 UI 自动化测试有什么关系?

wtnhz 回复

对照着我帖子里的大佬链接看,我这边帖子是为了简化设计模式学习,场景要去看链接里的帖子,他写的代码有点复杂,而且是 Java 的,对新手不友好

Kissshot 回复

是嘛,果然是新手懂新手

class Earth(object):    
    __instance=None #定义一个类属性做判断     
    def __new__(cls):         
        if cls.__instance==None:            
            #如果__instance为空证明是第一次创建实例            
            #通过父类的__new__(cls)创建实例                                             
            cls.__instance==object.__new__(cls)            
            return  cls.__instance        
        else:            
            #返回上一个对象的引用            
            return cls.__instance 
a = Earth()
print(id(a))
b = Earth()
print(id(b))

难道就没发现 a是 None 么?这段代码网上多的是,一错全错

hellohell 回复

看了思路感觉还行,简单易懂,没发现是 none 啊,大佬说下怎么看出来的

难道就不能最后加一个 assert a is None执行一下么?

楼主可以再讲一下怎么和 UI 测试结合吗 举个简单的登录例子就好

hellohell 回复

不是 none

兄弟,你这个行为非常棒,例子也很简洁,但是那个策略模式是不是有些问题呀,貌似不是你描述的那样哦

我觉得自动化本质的目的是降低成本,这样弄 不是更复杂了么?

青谷 回复

熟能生巧,熟悉了设计模式不会复杂,而是自然而然的重构,自动化测试项目起来后维护成本非常大,特别是 ui 方面。所以框架设计不能少,我接手过接近新手写的代码,几乎不可读,不可维护,只能用,但愿不出问题,一出问题我也解决不了,只能推倒重来

青谷 回复

线上几万条 ui 用例跑批,稳定,可扩展是大头

红客联盟 回复

有存在几万条 UI 用例的情况? 这种情况 我感觉是测试方案设计有问题

3楼 已删除
红客联盟 回复

cls.instance==object.new__(cls)

这句应该是赋值,而非 连等于

挺好的,设计模式其实就是让框架更加易用、好改造的思想。最近我也在负责我们公司的自动化构造,框架的设计离不开设计模式

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册