每周一篇,“挑战!一句话编写自动化测试脚本”。今天我们来编写da麦网购买演唱会门票的自动化脚本(脚本仅供测试使用,请勿非合规使用)
前提
-
DeepSeek网站
-
提示词+提示文档
-
搭建好的自动化测试项目+测试环境
步骤
1、梳理自动化流程
首先去被测试系统,人工梳理一下自动化流程
注意:梳理时,建议直接找到对应的唯一前端元素(如class="input-search"
),可大大增强代码一次性成功概率
2、写好提示词
-
提示词需指定浏览器
-
指定脚本名称
-
将流程以阿拉伯数字1-10,按照点击元素的顺序编写
例如:
根据项目结构和项目示例代码,编写自动化测试代码。要求:使用edge浏览器,新建damai文件夹,并以damai.py,damai.json,test_damai.py等形式为脚本命名 1、打开URL=https://www.damai.cn/?spm=a2oeg.search_category.top.dhome.5bde4d15hqgU4d网址, 2、找到class="input-search"的元素,并输入文本“法老「生于未来」”; 3、点击class="btn-search"元素; 4、点击class="items__txt__title"元素下边的<a>标签; 5、向下滑动至class="buy-link"元素,并点击该元素; 6、断言:如果成功跳转至登录页面则成功,否则断言失败。
3、DeepSeek
将提示词和提示文档上传DeepSeek中
注意:“深度思考”建议打开,可以看到DeepSeek思考过程
4、粘贴代码到项目
按照DeepSeek给出的代码路径,粘贴代码至项目对应位置即可
5、运行,完成
在 scripts/tieba/test_sign.py 中右击使用pytest运行即可
自动化测试的项目框架可以根据小编以前的文章来自己搭建,当然也提供了懒人版一键下载
懒人版包含了自动化项目源码(采用selenium+pytest+allure,PO模型,数据分离的2025最新框架),DeepSeek提示文档(生成的代码可直接粘贴至源码运行,非常关键!),提示词(用来生成自动化测试脚本)
懒人版下载链接:https://www.ldxp.cn/item/g5ajbk
问题:
本次da麦网自动化遇到一个很奇怪的Bug,有了解的小伙伴一块讨论一下。
该脚本用于国内网络的时候会抛出错误 ReadTimeoutError
,但国外就一点问题没有。为啥呢?
完整报错信息
/01-Develop/01-code/code_shopping/venv/bin/Python /Applications/PyCharm.app/Contents/plugins/python/helpers/pycharm/_jb_pytest_runner.py --target test_damai.py::TestDamai.test_search_ticket
Testing started at 2:54 PM ...
Launching pytest with arguments test_damai.py::TestDamai::test_search_ticket --no-header --no-summary -q in /01-Develop/01-code/code_shopping/scripts/damai
============================= test session starts ==============================
collecting ... collected 1 item
test_damai.py::TestDamai::test_search_ticket
================== 1 failed, 2 warnings in 126.96s (0:02:06) ===================
FAILED
scripts/damai/test_damai.py:27 (TestDamai.test_search_ticket)
self = <urllib3.connectionpool.HTTPConnectionPool object at 0x102035c10>
conn = <urllib3.connection.HTTPConnection object at 0x102035f40>
method = 'POST'
url = '/session/9cc5d627107c71b233ec1906df2c98d3/element/f.EE709217E43B2ED10833B704593AAC97.d.47A11EB6D3EA0964E807B1F824F2492D.e.59/click'
body = '{}'
headers = HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json;charset=UTF-8', 'User-Agent': 'selenium/4.31.0 (python mac)', 'Connection': 'keep-alive'})
retries = Retry(total=3, connect=None, read=None, redirect=None, status=None)
timeout = Timeout(connect=120, read=120, total=None), chunked = False
response_conn = None, preload_content = True, decode_content = True
enforce_content_length = True
def _make_request(
self,
conn: BaseHTTPConnection,
method: str,
url: str,
body: _TYPE_BODY | None = None,
headers: typing.Mapping[str, str] | None = None,
retries: Retry | None = None,
timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
chunked: bool = False,
response_conn: BaseHTTPConnection | None = None,
preload_content: bool = True,
decode_content: bool = True,
enforce_content_length: bool = True,
) -> BaseHTTPResponse:
"""
Perform a request on a given urllib connection object taken from our
pool.
:param conn:
a connection from one of our connection pools
:param method:
HTTP request method (such as GET, POST, PUT, etc.)
:param url:
The URL to perform the request on.
:param body:
Data to send in the request body, either :class:`str`, :class:`bytes`,
an iterable of :class:`str`/:class:`bytes`, or a file-like object.
:param headers:
Dictionary of custom headers to send, such as User-Agent,
If-None-Match, etc. If None, pool headers are used. If provided,
these headers completely replace any pool-specific headers.
:param retries:
Configure the number of retries to allow before raising a
:class:`~urllib3.exceptions.MaxRetryError` exception.
Pass ``None`` to retry until you receive a response. Pass a
:class:`~urllib3.util.retry.Retry` object for fine-grained control
over different types of retries.
Pass an integer number to retry connection errors that many times,
but no other types of errors. Pass zero to never retry.
If ``False``, then retries are disabled and any exception is raised
immediately. Also, instead of raising a MaxRetryError on redirects,
the redirect response will be returned.
:type retries: :class:`~urllib3.util.retry.Retry`, False, or an int.
:param timeout:
If specified, overrides the default timeout for this one
request. It may be a float (in seconds) or an instance of
:class:`urllib3.util.Timeout`.
:param chunked:
If True, urllib3 will send the body using chunked transfer
encoding. Otherwise, urllib3 will send the body using the standard
content-length form. Defaults to False.
:param response_conn:
Set this to ``None`` if you will handle releasing the connection or
set the connection to have the response release it.
:param preload_content:
If True, the response's body will be preloaded during construction.
:param decode_content:
If True, will attempt to decode the body based on the
'content-encoding' header.
:param enforce_content_length:
Enforce content length checking. Body returned by server must match
value of Content-Length header, if present. Otherwise, raise error.
"""
self.num_requests += 1
timeout_obj = self._get_timeout(timeout)
timeout_obj.start_connect()
conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout)
try:
# Trigger any extra validation we need to do.
try:
self._validate_conn(conn)
except (SocketTimeout, BaseSSLError) as e:
self._raise_timeout(err=e, url=url, timeout_value=conn.timeout)
raise
# _validate_conn() starts the connection to an HTTPS proxy
# so we need to wrap errors with 'ProxyError' here too.
except (
OSError,
NewConnectionError,
TimeoutError,
BaseSSLError,
CertificateError,
SSLError,
) as e:
new_e: Exception = e
if isinstance(e, (BaseSSLError, CertificateError)):
new_e = SSLError(e)
# If the connection didn't successfully connect to it's proxy
# then there
if isinstance(
new_e, (OSError, NewConnectionError, TimeoutError, SSLError)
) and (conn and conn.proxy and not conn.has_connected_to_proxy):
new_e = _wrap_proxy_error(new_e, conn.proxy.scheme)
raise new_e
# conn.request() calls http.client.*.request, not the method in
# urllib3.request. It also calls makefile (recv) on the socket.
try:
conn.request(
method,
url,
body=body,
headers=headers,
chunked=chunked,
preload_content=preload_content,
decode_content=decode_content,
enforce_content_length=enforce_content_length,
)
# We are swallowing BrokenPipeError (errno.EPIPE) since the server is
# legitimately able to close the connection after sending a valid response.
# With this behaviour, the received response is still readable.
except BrokenPipeError:
pass
except OSError as e:
# MacOS/Linux
# EPROTOTYPE and ECONNRESET are needed on macOS
# https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/
# Condition changed later to emit ECONNRESET instead of only EPROTOTYPE.
if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET:
raise
# Reset the timeout for the recv() on the socket
read_timeout = timeout_obj.read_timeout
if not conn.is_closed:
# In Python 3 socket.py will catch EAGAIN and return None when you
# try and read into the file pointer created by http.client, which
# instead raises a BadStatusLine exception. Instead of catching
# the exception and assuming all BadStatusLine exceptions are read
# timeouts, check for a zero timeout before making the request.
if read_timeout == 0:
raise ReadTimeoutError(
self, url, f"Read timed out. (read timeout={read_timeout})"
)
conn.timeout = read_timeout
# Receive the response from the server
try:
> response = conn.getresponse()
../../venv/lib/python3.9/site-packages/urllib3/connectionpool.py:534:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../venv/lib/python3.9/site-packages/urllib3/connection.py:516: in getresponse
httplib_response = super().getresponse()
/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/client.py:1349: in getresponse
response.begin()
/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/client.py:316: in begin
version, status, reason = self._read_status()
/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/client.py:277: in _read_status
line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <socket.SocketIO object at 0x1020568b0>, b = <memory at 0x101f93a00>
def readinto(self, b):
"""Read up to len(b) bytes into the writable buffer *b* and return
the number of bytes read. If the socket is non-blocking and no bytes
are available, None is returned.
If *b* is non-empty, a 0 return value indicates that the connection
was shutdown at the other end.
"""
self._checkClosed()
self._checkReadable()
if self._timeout_occurred:
raise OSError("cannot read from timed out object")
while True:
try:
> return self._sock.recv_into(b)
E socket.timeout: timed out
/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socket.py:704: timeout
The above exception was the direct cause of the following exception:
self = <test_damai.TestDamai object at 0x101f988b0>
@allure.story("搜索法老演唱会")
@allure.title("大麦网搜索购票测试")
@allure.description("测试搜索法老演唱会并验证登录跳转")
def test_search_ticket(self):
# 创建页面对象
page_damai = PageDamai(self.driver)
# 执行搜索流程
> page_damai.page_search_flow(
url=self.data_json[0],
keyword=self.data_json[1]
)
test_damai.py:36:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../page/damai/damai.py:64: in page_search_flow
self.page_click_search_btn()
../../page/damai/damai.py:28: in page_click_search_btn
self.common_click(damai.search_btn)
../../common/common.py:45: in common_click
self.common_find_html_element(loc).click()
../../venv/lib/python3.9/site-packages/selenium/webdriver/remote/webelement.py:119: in click
self._execute(Command.CLICK_ELEMENT)
../../venv/lib/python3.9/site-packages/selenium/webdriver/remote/webelement.py:572: in _execute
return self._parent.execute(command, params)
../../venv/lib/python3.9/site-packages/selenium/webdriver/remote/webdriver.py:427: in execute
response = self.command_executor.execute(driver_command, params)
../../venv/lib/python3.9/site-packages/selenium/webdriver/remote/remote_connection.py:404: in execute
return self._request(command_info[0], url, body=data)
../../venv/lib/python3.9/site-packages/selenium/webdriver/remote/remote_connection.py:428: in _request
response = self._conn.request(method, url, body=body, headers=headers, timeout=self._client_config.timeout)
../../venv/lib/python3.9/site-packages/urllib3/_request_methods.py:143: in request
return self.request_encode_body(
../../venv/lib/python3.9/site-packages/urllib3/_request_methods.py:278: in request_encode_body
return self.urlopen(method, url, **extra_kw)
../../venv/lib/python3.9/site-packages/urllib3/poolmanager.py:443: in urlopen
response = conn.urlopen(method, u.request_uri, **kw)
../../venv/lib/python3.9/site-packages/urllib3/connectionpool.py:841: in urlopen
retries = retries.increment(
../../venv/lib/python3.9/site-packages/urllib3/util/retry.py:474: in increment
raise reraise(type(error), error, _stacktrace)
../../venv/lib/python3.9/site-packages/urllib3/util/util.py:39: in reraise
raise value
../../venv/lib/python3.9/site-packages/urllib3/connectionpool.py:787: in urlopen
response = self._make_request(
../../venv/lib/python3.9/site-packages/urllib3/connectionpool.py:536: in _make_request
self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <urllib3.connectionpool.HTTPConnectionPool object at 0x102035c10>
err = timeout('timed out')
url = '/session/9cc5d627107c71b233ec1906df2c98d3/element/f.EE709217E43B2ED10833B704593AAC97.d.47A11EB6D3EA0964E807B1F824F2492D.e.59/click'
timeout_value = 120
def _raise_timeout(
self,
err: BaseSSLError | OSError | SocketTimeout,
url: str,
timeout_value: _TYPE_TIMEOUT | None,
) -> None:
"""Is the error actually a timeout? Will raise a ReadTimeout or pass"""
if isinstance(err, SocketTimeout):
> raise ReadTimeoutError(
self, url, f"Read timed out. (read timeout={timeout_value})"
) from err
E urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='localhost', port=50828): Read timed out. (read timeout=120)
../../venv/lib/python3.9/site-packages/urllib3/connectionpool.py:367: ReadTimeoutError
进程已结束,退出代码为 1