【求助】企业微信实战优化po后的问题

听完企业微信实战(三)的直播课后,按照老师的步骤在实战(二)的基础上进行代码改造,把driver私有化,增加find_element、find_elements和显式等待,改造完成并且成功运行,但是重复执行几次后,有几次运行很快,有几次运行半天还在转圈,只能停止运行,感觉奇怪的我就查看了进程,然后发现进程中有一堆的chromedriver,我就想着加上quit方法,结果问题仍然存在。去群里求助,同学和老师都给出了建议,遗憾的是我实在找不到解决方案,所以来此发帖求助。下面是我写的代码:

test_add_member.py:

from time import sleep

from webchat.page.index import Index


class TestAddMember:
    def setup(self):
        self.index = Index(reuse=True)

    def teardown(self):
        self.index.driver_quit()
        print('teardown')

    def test_add_member(self):
        member = self.index.goto_add_member()
        # 测试添加成员
        member.add_member()

        sleep(2)
        # 获取通讯录表格中所有成员的姓名
        names = member.get_table_names()
        # 断言新添加的成员姓名在通讯录中
        assert 'test_add' in names

add_member.py:

from selenium.webdriver.common.by import By

from webchat.page.base_page import BasePage


class AddMember(BasePage):
    # 添加成员操作
    def add_member(self):
        # 添加姓名
        self.find_element(By.ID, "username").send_keys("test_add")
        # 添加账号
        self.find_element(By.ID, "memberAdd_acctid").send_keys("hogwarts")
        # 添加手机号
        self.find_element(By.ID, "memberAdd_phone").send_keys("12345678910")
        # 点击保存按钮
        self.find_element(By.CSS_SELECTOR, ".js_btn_save").click()

    # 获取通讯录界面table里所有姓名的值
    def get_table_names(self):
        names = []
        # td:nth-child(2):获取td父元素的第二个子元素,即获取tr的第二个td,也就是姓名
        td_names = self.find_elements(By.CSS_SELECTOR, "#member_list td:nth-child(2)")
        for td in td_names:
            # 获取姓名的值
            title = td.get_attribute("title")
            names.append(title)
        return names

index.py:

from selenium.webdriver.common.by import By

from webchat.page.add_member import AddMember
from webchat.page.base_page import BasePage


class Index(BasePage):
    # 打开企业微信首页(需要浏览器已登录企业微信)
    base_url = "https://work.weixin.qq.com/wework_admin/frame"

    def goto_add_member(self):
        # 点击通讯录按钮
        self.find_element(By.ID, "menu_contacts").click()

        def wait(x):
            # 页面中是否有姓名输入框
            e_len = len(self.find_elements(By.ID, "username"))
            if e_len < 1:
                # 如果没有,则点击添加成员按钮
                self.find_element(By.CSS_SELECTOR, ".ww_operationBar:nth-child(1) > .js_add_member").click()
            return e_len > 0
        # 显式等待
        self.wait_for(wait)
        # 跳转至添加成员页(对AddMember进行了实例化)
        return AddMember(reuse=True)

base_page.py:

from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait


class BasePage:
    _driver = ""
    base_url = ""

    def __init__(self, reuse=False):
        if reuse:
            # 开启本地9222端口调试
            chrome_opts = webdriver.ChromeOptions()
            chrome_opts.debugger_address = "localhost:9222"
            self._driver = webdriver.Chrome(options=chrome_opts)
        else:
            self._driver = webdriver.Chrome()
        if self.base_url != "":
            self._driver.get(self.base_url)
        # 隐式等待
        self._driver.implicitly_wait(3)

    def driver_quit(self):
        self._driver.quit()
        print('exc driver quit')

    def find_element(self, by, value):
        return self._driver.find_element(by, value)

    def find_elements(self, by, value):
        return self._driver.find_elements(by, value)

    def wait_for(self, func):
        WebDriverWait(self._driver, 10).until(func)

之前只关注了quit是否执行,根据控制台的信息,quit确实执行了,但是进程并没有并关掉,后来在BasePage__init__方法中打印self._driver发现了问题,如下图:

根据图中打印的信息,貌似是index.py的return AddMember(reuse=True)和test_add_member.py的self.index = Index(reuse=True)这两行代码都走了reuse为True的方法,即执行了两遍下列代码:

chrome_opts = webdriver.ChromeOptions()
chrome_opts.debugger_address = "localhost:9222"
self._driver = webdriver.Chrome(options=chrome_opts)

所以每次运行都生成了两个self._driver,quit方法只退出了第一次生成的那个self._driver,第二次生成的self._driver在进程中保留,所以每执行一次,进程中就多一个chromedriver。

然后我又想到,如果是这样的话,那么我之前没加quit方法的时候,岂不是每次执行都多出两个chromedriver的进程。。。 但是当我去掉quit方法运行观察后,加不加quit方法都是每次执行多出一个chromedriver的进程,实在不知道什么原因,无从下手:sob: :sob: :sob: :sob: :sob:

1 个赞

个人有个想法,是不是能够利用python的析构函数__del__来执行关闭浏览器的操作,代码如下:

from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait


class BasePage:
    _driver = ""
    base_url = ""

    def __init__(self, reuse=False):
        if reuse:
            # 开启本地9222端口调试
            chrome_opts = webdriver.ChromeOptions()
            chrome_opts.debugger_address = "localhost:9222"
            self._driver = webdriver.Chrome(options=chrome_opts)
        else:
            self._driver = webdriver.Chrome()
        if self.base_url != "":
            self._driver.get(self.base_url)
        # 隐式等待
        self._driver.implicitly_wait(3)

    def driver_quit(self):
        self._driver.quit()
        print('exc driver quit')

    def find_element(self, by, value):
        return self._driver.find_element(by, value)

    def find_elements(self, by, value):
        return self._driver.find_elements(by, value)

    def wait_for(self, func):
        WebDriverWait(self._driver, 10).until(func)

    def __del__(self):
        self._driver.quit()

这样,我们手动在driver对象引用为0 的时候调用quit方法,那么只要我不再使用当前的driver就将对象释放,是不是可以实现类似操作。

好吧,以上想法作废,在page比较多的豆瓣上进行了尝试,用例执行完之前就会关闭浏览器,导致后面的用例执行不下去了

原因已经通过debug定位出来了
就是,return AddMember(reuse=True)
base_page的_init_函数又执行了一次。执行的是图片第一个红圈的分支。


所以会再次唤起Chrome()对象,再打开一次浏览器。
然后尝试将reuse改为False,发现else分支也会做一样的事情。除非把else分支的command注释掉改为pass。但是和设计模式的设计初衷相悖了。

这部分的优化还需要和老师再讨论一下

好的,辛苦啦 :grinning:

感谢 :grinning:

  1. 在声明新的 driver 前,退出旧的 driver
  2. all kill
  3. 在参数中加一个 driver 传进来,实现复用
  4. 单例模式

我尝试了使用__del__方法,有时候可以解决进程未退出的问题:

有时候也会出现TimeoutException的错误,应该是你说的提前关闭导致的问题

Testing started at 15:22 ...
/Users/heng/develop/PycharmProjects/WebChatTest/venv/bin/python "/Users/heng/Library/Application Support/JetBrains/Toolbox/apps/PyCharm-P/ch-0/192.7142.79/PyCharm.app/Contents/helpers/pycharm/_jb_pytest_runner.py" --target test_add_member.py::TestAddMember
Launching pytest with arguments test_add_member.py::TestAddMember in /Users/heng/develop/PycharmProjects/WebChatTest/webchat/testcase

============================= test session starts ==============================
platform darwin -- Python 3.8.2, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /Users/heng/develop/PycharmProjects/WebChatTest/venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/heng/develop/PycharmProjects/WebChatTest/webchat/testcase
collecting ... collected 1 item

test_add_member.py::TestAddMember::test_add_member setup self.index = Index(reuse=True)
<selenium.webdriver.chrome.webdriver.WebDriver (session="e08bf6ea94f4bc6f68650e927f5c2934")>
FAILED                [100%]
test_add_member.py:14 (TestAddMember.test_add_member)
self = <testcase.test_add_member.TestAddMember object at 0x103572940>

    def test_add_member(self):
>       member = self.index.goto_add_member()

test_add_member.py:16: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../page/index.py:25: in goto_add_member
    self.wait_for(wait)
../page/base_page.py:42: in wait_for
    WebDriverWait(self._driver, 10).until(func)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.support.wait.WebDriverWait (session="e08bf6ea94f4bc6f68650e927f5c2934")>
method = <function Index.goto_add_member.<locals>.wait at 0x103913a60>
message = ''

    def until(self, method, message=''):
        """Calls the method provided with the driver as an argument until the \
        return value is not False."""
        screen = None
        stacktrace = None
    
        end_time = time.time() + self._timeout
        while True:
            try:
                value = method(self._driver)
                if value:
                    return value
            except self._ignored_exceptions as exc:
                screen = getattr(exc, 'screen', None)
                stacktrace = getattr(exc, 'stacktrace', None)
            time.sleep(self._poll)
            if time.time() > end_time:
                break
>       raise TimeoutException(message, screen, stacktrace)
E       selenium.common.exceptions.TimeoutException: Message:

../../venv/lib/python3.8/site-packages/selenium/webdriver/support/wait.py:80: TimeoutException
<selenium.webdriver.chrome.webdriver.WebDriver (session="e08bf6ea94f4bc6f68650e927f5c2934")>
quit
teardown self.index.driver_quit()

Assertion failed

Assertion failed


=================================== FAILURES ===================================
________________________ TestAddMember.test_add_member _________________________

self = <testcase.test_add_member.TestAddMember object at 0x103572940>

    def test_add_member(self):
>       member = self.index.goto_add_member()

test_add_member.py:16: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../page/index.py:25: in goto_add_member
    self.wait_for(wait)
../page/base_page.py:42: in wait_for
    WebDriverWait(self._driver, 10).until(func)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.support.wait.WebDriverWait (session="e08bf6ea94f4bc6f68650e927f5c2934")>
method = <function Index.goto_add_member.<locals>.wait at 0x103913a60>
message = ''

    def until(self, method, message=''):
        """Calls the method provided with the driver as an argument until the \
        return value is not False."""
        screen = None
        stacktrace = None
    
        end_time = time.time() + self._timeout
        while True:
            try:
                value = method(self._driver)
                if value:
                    return value
            except self._ignored_exceptions as exc:
                screen = getattr(exc, 'screen', None)
                stacktrace = getattr(exc, 'stacktrace', None)
            time.sleep(self._poll)
            if time.time() > end_time:
                break
>       raise TimeoutException(message, screen, stacktrace)
E       selenium.common.exceptions.TimeoutException: Message:

../../venv/lib/python3.8/site-packages/selenium/webdriver/support/wait.py:80: TimeoutException
---------------------------- Captured stdout setup -----------------------------
setup self.index = Index(reuse=True)
<selenium.webdriver.chrome.webdriver.WebDriver (session="e08bf6ea94f4bc6f68650e927f5c2934")>
--------------------------- Captured stdout teardown ---------------------------
<selenium.webdriver.chrome.webdriver.WebDriver (session="e08bf6ea94f4bc6f68650e927f5c2934")>
quit
teardown self.index.driver_quit()
=========================== short test summary info ============================
FAILED test_add_member.py::TestAddMember::test_add_member - selenium.common.e...
========================= 1 failed in 79.95s (0:01:19) =========================

Process finished with exit code 0

Assertion failed

Assertion failed

Assertion failed

Assertion failed

我之前一直在尝试只让self._driver = webdriver.Chrome(options=chrome_opts)执行一次,想了各种办法,结果都失败了。

我目前看到的情况是Index实例化的时候会生成一个self._driver,AddMember实例化的时候也会生成一个self._driver,但是我找不到关联它们的办法。

比如下图中我把self._driver默认置为None,执行self.index = Index(reuse=True)的之后self._driver是有值的,但是在执行return AddMember(reuse=True)的时候,self._driver又变成了None,无法记录index实例化的那个self._driver

### 更新:之前的TimeoutException应该是网页加载出了一些问题,增加如下代码后,连续运行十多次都没有再出现TimeoutException,大部分都正常执行并退出进程,但是偶尔还是会多出一个进程

desired_capabilities = DesiredCapabilities.CHROME
desired_capabilities["pageLoadStrategy"] = "none"

再次更新,之前可能是我操作不当引起的进程残留(运行期间我使用了浏览器和命令行,可能会哪里产生影响了)。刚才又连续运行了十多次,没有做别的,一切顺利,进程也没有残留。目前来说,暂时告一段落了,以后再发现问题我继续更贴