Software testing and development combat | Record several pits stepped on when writing decorators

This article is reproduced from the study notes of indeyo, an excellent student at Hogwarts Testing Institute, the original link:
http://qrcode.testing-studio.com/f?from=51cto&url=https://ceshiren.com/tag/ %E7%B2%BE%E5%8D%8E%E5%B8%96 Please indicate the source for reprinting

background

Decorator is a very useful Syntactic Sugar in python, which can reduce the writing of a lot of repetitive code.
I just recently learned the exception handling of the app automation framework. There is a certain amount of repetitive code. I am going to use it as a theme to practice the decorator.
Let's record the path of the decorator.

坑 1:Hint: make sure your test modules/packages have valid Python names.

Error message

test_market.py:None (test_market.py)
ImportError while importing test module 'D:\project\Hogwarts_11\test_appium\testcase\test_market.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
test_market.py:9: in <module>
    from test_appium.page.app import App
..\page\app.py:12: in <module>
    from test_appium.page.base_page import BasePage
..\page\base_page.py:16: in <module>
    from test_appium.utils.exception import exception_handle
..\utils\exception.py:11: in <module>
    from test_appium.page.base_page import BasePage
E   ImportError: cannot import name 'BasePage' from 'test_appium.page.base_page' (D:\project\Hogwarts_11\test_appium\page\base_page.py)

the reason

There is a mutual calling relationship between the exception.py file and the base_page.py file.

solution

Put the package import information of the loop call in the function. As long as the reference information of one party is placed in the function, it is not necessary to put both sides.
I only changed the exception.py file, base_page.py remained unchanged.

exception.py

def exception_handle(func):
    def magic(*args, **kwargs):
        # 防止循环调用报错
        from test_appium.page.base_page import BasePage
        # 获取BasePage实例对象的参数self,这样可以复用driver
        _self: BasePage = args[0]
...

坑 2:IndexError: tuple index out of range

Error message

test_search.py:None (test_search.py)
test_search.py:11: in <module>
    from test_appium.page.app import App
..\page\app.py:12: in <module>
    from test_appium.page.base_page import BasePage
..\page\base_page.py:52: in <module>
    class BasePage:
..\page\base_page.py:74: in BasePage
    def find(self, locator, key=None):
..\page\base_page.py:50: in exception_handle
    return magic()
..\page\base_page.py:24: in magic
    _self: BasePage = args[0]
E   IndexError: tuple index out of range

the reason

It’s really easy to make this mistake when writing decorators for the first time.

def decorator(func):
    def magic(*args, **kwargs):
        _self: BasePage = args[0]
        ...
        return magic(*args, **kwargs)
    # 这里的问题!!!不应该返回函数调用,要返回函数名称!!!
    return magic()

Why does the return function call report this error?
Because when the magic() function is called, no parameters are passed in, but the input parameters are quoted in magic(). At this time, args has no value, so args[0] is naturally not available.

solution

Just remove the brackets

def decorator(func):
    def magic(*args, **kwargs):
        _self: BasePage = args[0]
        ...
        return magic(*args, **kwargs)
    # 返回函数名,即函数本身
    return magic

Pit 3: The exception handling is only executed once, and automation cannot continue

Error message

Mainly various exceptions in the process of locating elements, such as NoSuchElementException, TimeoutException and other common problems.

the reason

After exception handling, the recursive logic is written incorrectly. return func() executes func() and jumps out of the exception handling logic, so the exception handling is executed only once.
The correct way of writing is return magic().
It feels like a mistake made by the decorator Xiaobai again...emmm...

solution

For intuition, unimportant code has been filtered, and the exception handling logic code will be released at the end of the article.

def exception_handle(func):
    def magic(*args, **kwargs):
        _self: BasePage = args[0]
        try:
            return func(*args, **kwargs)
        # 弹窗等异常处理逻辑
        except Exception as e:
            for element in _self._black_list:
                elements = _self._driver.find_elements(*element)
                if len(elements) > 0:
                    elements[0].click()
                    # 异常处理结束,递归继续查找元素 
                    # 这里之前写成了return func(*args, **kwargs),所以异常只执行一次!!!!!
                    return magic(*args, **kwargs)
            raise e
    return magic

Pit 4: How to reuse the driver?

problem

When I first tried to write a decorator, I found a problem.
Find_elements is needed in the decorator. Where does the driver come from? And how to get the private variables error_max and error_count of BasePage? Create a BasePage object? Then pass the driver through the func function?
The driver of func is private and cannot be called externally (it turns out that it can be emmm...).
I tried to make the abnormally related variables public, but it was useless, and I still couldn't solve the problem of calling find_elements.

solution

Sihan's approach is to create a self variable in the decorator and take args[0], which is the first parameter self of the function func.
_self: BasePage = args[0] This simple sentence successfully answered all my questions.
In the class function definition, self represents the class itself, so you can get the ._driver attribute and call find_elements.

坑 5:AttributeError

Report an error when you are about to click after finding the element

Error message

EINFO:root:('id', 'tv_search')
INFO:root:None
INFO:root:('id', 'image_cancel')
INFO:root:('id', 'tv_agree')
INFO:root:('id', 'tv_search')
INFO:root:None

test setup failed
self = <test_appium.testcase.test_search.TestSearch object at 0x0000018946B70940>

    def setup(self):
>       self.page = App().start().main().goto_search()

test_search.py:16: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <test_appium.page.main.MainPage object at 0x0000018946B70780>

    def goto_search(self):
>       self.find(self._search_locator).click()
E       AttributeError: 'NoneType' object has no attribute 'click'

..\page\main.py:20: AttributeError

the reason

After looking at the find function, after finding the element, it returns the element itself

@exception_handle
    def find(self, locator, key=None):
        logging.info(locator)
        logging.info(key)
        # 定位符支持元组格式和两个参数格式
        locator = locator if isinstance(locator, tuple) else (locator, key)
        WebDriverWait(self._driver, 10).until(expected_conditions.visibility_of_element_located(locator))
        element = self._driver.find_element(*locator)
        return element

That is, the decorator is wrong

def exception_handle(func):
    def magic(*args, **kwargs):
        _self: BasePage = args[0]
        try:
            # 这里只是执行了函数,但是没有return
            func(*args, **kwargs)
        # 弹窗等异常处理逻辑
        except Exception as e:
            raise e
    return magic

solution

The function call must be returned in the decorator, or the return of the function itself will be eaten by the decorator.

def exception_handle(func):
    def magic(*args, **kwargs):
        _self: BasePage = args[0]
        try:
            # return函数执行结果
            return func(*args, **kwargs)
        # 弹窗等异常处理逻辑
        except Exception as e:
            raise e
    return magic

Thinking: When writing decorators, various returns looked a little dizzy. Each function can be returned, what does it mean? ? ?

def exception_handle(func):
    def magic(*args, **kwargs):
        _self: BasePage = args[0]
        try:
            # 第1处 return:传递func()函数的返回值。如果不写,原有return则失效
            return func(*args, **kwargs)
        # 弹窗等异常处理逻辑
        except Exception as e:
            for element in _self._black_list:
                elements = _self._driver.find_elements(*element)
                if len(elements) > 0:
                    elements[0].click()
                    # 异常处理结束,递归继续查找元素 
                    # 第2处 return:递归调用装饰后的函数。magic()表示新函数,func()表示原函数,不可混淆
                    return magic(*args, **kwargs)
            raise e
    # 第3处 return:返回装饰后的函数,装饰器语法。不能返回函数调用magic()
    return magic

Decorator complete implementation

exception.py

import logging

logging.basicConfig(level=logging.INFO)

def exception_handle(func):
    def magic(*args, **kwargs):
        # 防止循环调用报错
        from test_appium.page.base_page import BasePage
        # 获取BasePage实例对象的参数self,这样可以复用driver
        _self: BasePage = args[0]
        try:
            # logging.info('error count is %s' % _self._error_count)
            result = func(*args, **kwargs)
            _self._error_count = 0
            # 返回调用函数的执行结果,要不然返回值会被装饰器吃掉
            return result
        # 弹窗等异常处理逻辑
        except Exception as e:
            # 如果超过最大异常处理次数,则抛出异常
            if _self._error_count > _self._error_max:
                raise e
            _self._error_count += 1
            for element in _self._black_list:
                # 用find_elements,就算找不到元素也不会报错
                elements = _self._driver.find_elements(*element)
                logging.info(element)
                # 是否找到弹窗
                if len(elements) > 0:
                    # 出现弹窗,点击掉
                    elements[0].click()
                    # 弹窗点掉后,重新查找目标元素
                    return magic(*args, **kwargs)
            # 弹窗也没有出现,则抛出异常
            logging.warning("no error is found")
            raise e
    return magic

Learning experience

It is best not to read Sihan's explanation first, and to write the decorator according to your own understanding, so that the learning effect is best.
I tried to solve the problem when I encountered the problem, and the pit I stepped on was impressive.
There is really no clue to refer to Sihan's solution again, and then there will be a sudden sense of clarity.
I have stepped on these pits at present, if there are any omissions, please add them~

This article is reproduced from the study notes of indeyo, an excellent student at Hogwarts Testing Institute, the original link:
http://qrcode.testing-studio.com/f?from=51cto&url=https://ceshiren.com/tag/ %E7%B2%BE%E5%8D%8E%E5%B8%96 Please indicate the source for reprinting

Guess you like

Origin blog.51cto.com/14293469/2561925