通用技术 重构自动化 1 -- 在不同 iframe 中反复切换问题

flint · 2015年06月01日 · 最后由 陈恒捷 回复于 2015年06月02日 · 1212 次阅读

0. 前言
在这里记录一些在测试中遇到的问题以及我的解决办法,希望大家多提意见和建议,以便可以改善和进步。

1. 背景
有的时候在同一个页面中包含不同的 iframe,例如 page A 中包含 iframe B 和 C,而且需要在他们中间切换,例如 B -> C ->B。

2. 问题
这时候就会很头疼,因为 iframe 跟单独的 page 不同,在 web driver 里不能取到当前所在的 iframe ID,但 page 可以用他的 title 或者 url 分辨,而且需要先切换回主页面,再进入下一个 iframe。这样的话就需要在从 C 到 B 的时候记住 B 的 ID 或者在 case 中进行切换,也就是说在 iframe 这个类的外面还要有地方切换回去,但是 iframe ID 是这个 iframe 的属性,而在这个类的外面反复的调用就会让代码显得很凌乱,一旦这个页面进行了改动则其他很多方法或用例都会受到影响。举个例子先:

page_a.py
class PageA(BasePage):
    def __init__(self, driver):
        pass

class IFrameB(BasePage, driver):
    def __init__(self):
        self.driver = driver

    def button_b1(self):
        return self.driver.find_element_by_id("buttonb1")

    def button_b2(self):
        return self.driver.find_element_by_id("buttonb2")

class IFrameC(BasePage):
    def __init__(self):
        self.driver = driver

    def button_c(self):
        return self.driver.find_element_by_id("buttonc")

test.py
def test_page_and_iframe():
    driver = webdriver.Firefox()
    driver.get("http://www.pagea.com")
    driver.switch_to_frame("iframeb")
    iframe_b = IFrameB(driver)
    button_b1 = iframe_b.button_b1()
    button_b.click()
    driver.switch_to.default_content()
    driver.switch_to_frame("iframec")
    iframe_c = IFrameC(driver)
    button_c = iframe_c.button_c()
    driver.switch_to.default_content()
    driver.switch_to_frame("iframeb")
    iframe_b = IFrameB(driver)
    button_b2 = iframe_b.button_b2()

从上面的例子可以看到这个 iframe 以外还要记录需要切换的 iframe id,这样很不方便,而且代码变的很乱也不适用于维护。

3. 解决办法
其实自动化成功与否的关键,很大程度上是由他的维护成本决定的,所以我们要尽量减少由于页面或需求变更带来的改变。我们应该尽量把页面的信息保留在这个页面的类中,减少因为改变对于外部代码的破坏。首先我们需要在整个用例中维护一个当前 title 和 iframe id,然后我们定义一个装饰器, 这个装饰器的作用是检测当前的 title 和 iframe,如果和需要使用的类中定义的不一样,则切换并更新全局变量中的 title 和 iframe id:

def stay_window_frame(func):  
    def stay(cls):  
        if cls.env.driver.title != cls._title:  
            cls.env.driver.switch_to_window(cls._title)  
            if cls.env.current_frame == "None":  
                cls.env.current_frame = "default_content"  
        if cls.env.current_frame != cls._frame[-1]:  
            cls.env.driver.switch_to_default_content()  
            cls.env.current_frame = "default_content"  
            if cls._frame[-1] != "default_content":  
                for f in cls._frame:  
                    cls.env.driver.switch_to_frame(f)  
                cls.env.current_frame = cls._frame[-1]  
        ele = func(cls)  
        return ele  
    return stay

然后我们只需要在 page object 中指定他的 title 或 iframe id,并在使用 root element 的函数时使用这个装饰器。

page_a.py
class PageA(BasePage):
    def __init__(self, env):
        self.driver = env.driver
        self._title = ""

class IFrameB(BasePage, driver):
    def __init__(self, env):
        self.driver = env.driver
        self._iframe = ["iframeb"]

    @stay_window_frame
    def button_b1(self):
        return self.driver.find_element_by_id("buttonb1")

    @stay_window_frame
    def button_b2(self):
        return self.driver.find_element_by_id("buttonb2")

class IFrameC(BasePage):
    def __init__(self, env):
        self.driver = env.driver
        self._iframe = ["iframec"]

    @stay_window_frame
    def button_c(self):
        return self.driver.find_element_by_id("buttonc")

test.py
def test_page_and_iframe():
    driver = webdriver.Firefox()
    driver.get("http://www.pagea.com")
    iframe_b = IFrameB(driver)
    button_b1 = iframe_b.button_b1()
    button_b.click()
    iframe_c = IFrameC(driver)
    button_c = iframe_c.button_c()
    iframe_b = IFrameB(driver)
    button_b2 = iframe_b.button_b2()

这样可以看到,在 test 里面就变的清爽很多,而且即使 iframe 有任何变更,完全不用担心,只需要修改这个 iframe 所在的类就可以了。

共收到 6 条回复 时间 点赞
if cls._frame[-1] != "default_content":  
                for f in cls._frame:  
                    cls.env.driver.switch_to_frame(f)  

for 循环里面需要一个 break, 一 switch 过去,就没必要进行下次 for 循环判断了。

#1 楼 @lihuazhang 这个的目的是有的时候 frame 里套 frame,所以要依次 switch 进去。

#1 楼 @lihuazhang 我明白他的意思了。例如 frame1 包着 frame2,frame2 包着 frame3,那必须先切到 frame1,然后 frame2,最后 frame3,需要依次切换。此时这个 frame 对象的 self._iframe = ["frame1", "frame2", "frame3"]
这种方式很不错,可以把 frame 以类似 page 的方式来管理,不用再在 case 层级关心 frame 的层级关系。

PS:通过 decorator 模块可以批量在类的方法前面添加装饰器,这样就不用每个方法都加一模一样的装饰器那么麻烦了:

class BasePageMetaClass(type):
    def __new__(cls, clsname, bases, dict):
        if decorator:
            for name, method in dict.items():
                if not name.startswith('_'):
                    dict[name] = decorator(stay_window_frame, method)
        return type.__new__(cls, clsname, bases, dict)

class BasePage:
    __metaclass__ = BasePageMetaClass
    ...

#3 楼 @chenhengjie123 这个还真是学习了,以前还真没用过 metaclass 这个 magic word

#3 楼 @chenhengjie123 哦,是一层层的。我想如果并行就没有必要一直循环。

#4 楼 @flint Sorry,代码里有个地方写错了,只有 new-style class 能使用 __metaclass__,所以正确的应该是这么写:

class BasePageMetaClass(type):
    def __new__(cls, clsname, bases, dict):
        if decorator:
            for name, method in dict.items():
                if not name.startswith('_'):
                    dict[name] = decorator(stay_window_frame, method)
        return type.__new__(cls, clsname, bases, dict)

class BasePage(object):
    __metaclass__ = BasePageMetaClass
    ...
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册