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 Boom2 天前
2. 如何理解互联网七层模型?深度解析。
网络·自动化测试·测试开发·测试用例·自动化测试框架开发
Tom Boom10 天前
Pytest断言全解析:掌握测试验证的核心艺术
自动化测试·python·测试开发·pytest
Tom Boom12 天前
自动化过程中,如何定位一闪而过的toast?
自动化测试·测试开发·自动化测试框架开发
Tom Boom15 天前
Selenium自动下载浏览器驱动
自动化测试·webdriver·自动化测试框架开发·自动下载驱动
Plus-ultra16 天前
近几年字节飞书测开部分面试题整理
测试开发
Tom Boom20 天前
43. 远程分布式测试实现
分布式·测试开发·自动化·webdriver·自动化测试框架开发·分布式测试
Tom Boom1 个月前
23. 装饰器应用之测试用例的依赖实现
服务器·网络·测试开发·测试用例·自动化测试框架开发·wraps
Tom Boom1 个月前
21. 自动化测试框架开发之Excel配置文件的测试用例改造
测试开发·selenium·测试工具·测试用例·excel·自动化测试框架开发·po改造
Tom Boom1 个月前
19. 结合Selenium和YAML对页面实例化PO对象改造
python·测试开发·selenium·测试工具·自动化测试框架开发·po改造