38. 自动化测试异步开发之编写客户端异步webdriver接口类

Selenium异步浏览器操作实现原理深度解析

一、AsyncBrowser类核心结构

1.1 类定义与启动方法

python 复制代码
class AsyncBrowser(Command):
    @classmethod
    async def start(cls, remote_driver_server: str,
                    capabilities: dict,
                    http_session: ClientSession,
                    reconnect_server_session: [None, str] = None):
        # 初始化实例
        self = cls()
        # 存储参数
        self._http_session = http_session
        self._remote_driver_server = remote_driver_server
        self._desired_capabilities = capabilities
        
        # 创建新会话或重用现有会话
        if reconnect_server_session is None:
            # 创建新会话
            async with self._http_session.post(...) as resp:
                r = await resp.json()
            self._browser_session_id = r['value'].get('sessionId')
        else:
            # 重用现有会话
            self._browser_session_id = reconnect_server_session
        
        # 构建会话URL
        self.url = f'{self._remote_driver_server}/session/{self._browser_session_id}'
        return self
启动参数说明
参数 类型 作用描述
remote_driver_server str WebDriver服务器地址
capabilities dict 浏览器能力配置
http_session ClientSession aiohttp会话对象
reconnect_server_session str/None 要重用的现有会话ID

二、核心功能实现

2.1 浏览器基础操作

python 复制代码
async def get(self, url: str):
    body = {'url': url}
    return await self.command('POST', endpoint='/url', json=body)

async def get_title(self):
    return await self.command('GET', endpoint='/title')

async def current_url(self):
    return await self.command('GET', endpoint='/url')

async def screenshot(self):
    return await self.command('GET', endpoint='/screenshot')

async def quit(self):
    return await self.command('DELETE', endpoint='')

2.2 元素定位与封装

python 复制代码
async def find_element(self, strategy: str, value: str, endpoint: str = '/element'):
    # 转换传统定位器为CSS选择器
    if strategy == 'id':
        strategy = 'css selector'
        value = f'[id="{value}"]'
    elif strategy == 'name':
        strategy = 'css selector'
        value = f'[name="{value}"]'
    elif strategy == 'class':
        strategy = 'css selector'
        value = f'.{value}'
    
    # 发送定位请求
    body = {'using': strategy, 'value': value}
    element_data = await self.command('POST', endpoint=endpoint, json=body)
    
    # 返回Element对象
    return Element(element_data, self._url, self._session)

2.3 元素操作快捷方法

python 复制代码
async def click(self, *location):
    element = await self.find_element(*location)
    return await element.click()

async def clear(self, *location):
    element = await self.find_element(*location)
    return await element.clear()

async def send_keys(self, *location, text: str):
    element = await self.find_element(*location)
    return await element.send_keys(text)

async def text(self, *location):
    element = await self.find_element(*location)
    return await element.text()

三、异步上下文管理器

python 复制代码
async def __aenter__(self):
    return self

async def __aexit__(self, *args):
    await self.quit()
  • 自动资源管理 :使用async with语法自动关闭浏览器
  • 异常安全:确保任何情况下都会执行退出操作
  • 简化代码:避免忘记手动调用quit()

四、实战使用示例

4.1 基础使用流程

python 复制代码
import asyncio
from aiohttp import ClientSession

async def main():
    async with ClientSession() as session:
        # 启动浏览器
        browser = await AsyncBrowser.start(
            remote_driver_server='http://localhost:9515',
            capabilities={
                'browserName': 'chrome',
                'goog:chromeOptions': {'args': ['--headless']}
            },
            http_session=session
        )
        
        # 访问网页
        await browser.get('https://example.com')
        
        # 获取页面标题
        title = await browser.get_title()
        print(f'页面标题: {title}')
        
        # 定位搜索框并输入
        await browser.send_keys('name', 'q', text='异步测试')
        
        # 定位按钮并点击
        await browser.click('css selector', 'input[type="submit"]')
        
        # 获取当前URL
        current_url = await browser.current_url()
        print(f'当前URL: {current_url}')
        
        # 自动关闭浏览器(通过上下文管理器)

# 运行示例
asyncio.run(main())

4.2 执行输出结果

text 复制代码
页面标题: Example Domain
当前URL: https://example.com/search?q=异步测试

五、设计原理分析

5.1 定位策略转换逻辑

原始策略 转换后策略 转换结果示例
id css selector [id="search"]
name css selector [name="username"]
class css selector .submit-btn
其他策略 保持不变 xpath, tag name等

5.2 会话管理机制

  1. 新会话创建

    http 复制代码
    POST /session
    {
      "capabilities": {...}
    }
  2. 会话重用:直接使用现有sessionId

  3. 会话销毁

    http 复制代码
    DELETE /session/:sessionId

5.3 协议端点映射

操作 HTTP方法 端点路径 参数
导航 POST /session/:id/url {"url": "..."}
获取标题 GET /session/:id/title
获取当前URL GET /session/:id/url
截图 GET /session/:id/screenshot

六、技术优势总结

  1. 完全异步设计

    • 基于aiohttp实现非阻塞IO
    • 支持高并发浏览器操作
  2. 资源自动管理

    • 上下文管理器确保资源释放
    • 避免浏览器进程泄漏
  3. 定位策略优化

    • 自动转换传统定位器为CSS选择器
    • 提高元素定位效率
  4. 协议标准化

    • 严格遵循W3C WebDriver协议
    • 兼容所有标准浏览器实现
  5. 操作链式封装

    • 提供元素操作快捷方法
    • 简化常见操作流程

这种设计模式展示了如何基于WebDriver协议构建现代异步浏览器自动化框架,为高效、可靠的自动化测试提供了坚实基础。

七、完整代码

python 复制代码
from selenium.webdriver.common.devtools.v133.runtime import await_promise

from chap9.async_http_client import Command
from aiohttp import ClientSession
from chap9.async_element import Element


class AsyncBrowser(Command):

    @classmethod
    async def start(cls, remote_driver_server: str,
                    capabilities: dict,
                    http_session: ClientSession,
                    reconnect_server_session: [None, str] = None):
        self = cls()
        self._http_session = http_session
        self._remote_driver_server = remote_driver_server
        self._desired_capabilities = capabilities

        if reconnect_server_session is None:
            async with self._http_session.post(f'{self._remote_driver_server}/session',
                                               json=self._desired_capabilities) as resp:
                r = await resp.json()
            self._browser_session_id = r['value'].get('sessionId', None) or r.get('sessionId', None)
        else:
            self._browser_session_id = reconnect_server_session
        self.url = f'{self._remote_driver_server}/session/{self._browser_session_id}'
        return self

    @property
    def _session_id(self):
        return getattr(self, '_browser_session_id')

    @property
    def _url(self):
        return getattr(self, 'url')

    @property
    def _session(self):
        return getattr(self, '_http_session')

    async def command(self, method: str, endpoint: str, **kwargs):
        await super(AsyncBrowser, self).command(method, self._url + endpoint, self._session, **kwargs)

    async def get(self, url: str):
        body = {
            'url': url
        }
        return await self.command('POST', endpoint='/url', json=body)

    async def find_element(self, strategy: str, value: str, endpoint: str = '/element'):
        if strategy == 'id':
            strategy = 'css selector'
            value = '[id="%s"]' % value
        elif strategy == 'name':
            strategy = 'css selector'
            value = '[name="%s"]' % value
        elif strategy == 'class':
            strategy = 'css selector'
            value = '.%s' % value
        body = {
            'using': strategy,
            'value': value
        }
        element = await self.command('POST', endpoint=endpoint, json=body)
        return Element(element, self._url, self._session)

    async def click(self, *location):
        element = await self.find_element(*location)
        return await element.click()

    async def clear(self, *location):
        element = await self.find_element(*location)
        return await element.clear()

    async def send_keys(self, *location, text: str):
        element = await self.find_element(*location)
        return await element.send_keys(text)

    async def text(self, *location):
        element = await self.find_element(*location)
        return await element.text()

    async def get_title(self):
        return await self.command('GET', endpoint='/title')

    async def current_url(self):
        return await self.command('GET', endpoint='url')

    async def screenshot(self):
        return await self.command('GET', endpoint='/screenshot')

    async def quit(self):
        return await self.command('DELETE', endpoint='')

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self.quit()

「小贴士」 :点击头像→【关注】按钮,获取更多软件测试的晋升认知不迷路! 🚀

相关推荐
Tom Boom7 天前
23. 装饰器应用之测试用例的依赖实现
服务器·网络·测试开发·测试用例·自动化测试框架开发·wraps
Tom Boom8 天前
21. 自动化测试框架开发之Excel配置文件的测试用例改造
测试开发·selenium·测试工具·测试用例·excel·自动化测试框架开发·po改造
Tom Boom13 天前
19. 结合Selenium和YAML对页面实例化PO对象改造
python·测试开发·selenium·测试工具·自动化测试框架开发·po改造
Tom Boom21 天前
14. 原生测试框架Unittest的skip、skipIf、skipUnless的使用
自动化测试·python·测试开发·unittest·自动化测试框架开发
demodeom1 个月前
常见浏览器 WebDriver 驱动下载
selenium·webdriver
阿华的代码王国3 个月前
【性能测试】Jmeter下载安装、环境配置-小白使用手册(1)
测试开发·jmeter·性能测试·测试·jmeter使用
LUCIAZZZ3 个月前
Java测试框架Mockito快速入门
java·测试开发·测试工具·spring·单元测试·log4j
霍格沃兹测试开发学社测试人社区3 个月前
性能测试丨JMeter 分布式加压机制
软件测试·分布式·测试开发·jmeter
霍格沃兹测试开发学社测试人社区3 个月前
人工智能丨卷积神经网络的概念, 它与普通的神经网络有何不同
软件测试·人工智能·测试开发·神经网络·cnn