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

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

一、AsyncBrowser类核心结构

1.1 类定义与启动方法

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_serverstrWebDriver服务器地址
capabilitiesdict浏览器能力配置
http_sessionClientSessionaiohttp会话对象
reconnect_server_sessionstr/None要重用的现有会话ID

二、核心功能实现

2.1 浏览器基础操作

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 元素定位与封装

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 元素操作快捷方法

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 __aenter__(self):
    return self

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

四、实战使用示例

4.1 基础使用流程

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 执行输出结果

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

五、设计原理分析

5.1 定位策略转换逻辑

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

5.2 会话管理机制

  1. 新会话创建
    POST /session
    {
      "capabilities": {...}
    }
    
  2. 会话重用:直接使用现有sessionId
  3. 会话销毁
    DELETE /session/:sessionId
    

5.3 协议端点映射

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

六、技术优势总结

  1. 完全异步设计

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

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

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

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

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

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

七、完整代码

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()


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值