python 常用 UI 自动化设计模式总结

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

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

page object设计模式

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

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

工厂模式

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

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

实例

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

#构造如下的虚拟股票查询器:
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

#根据不同的查询机构和方式来通过继承的方式实现其的股票查询器类。
#WebAWebB的查询器类可以构造如下:
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()