为什么pytest测试简单异步函数时第二个用例会出现挂起问题?
问题根因
- 端口占用冲突:你把服务端口硬编码为固定值
65432,第一个测试执行完fixture销毁服务时,TCP连接会进入TIME_WAIT状态,端口不会立即释放,第二个测试再启动服务绑定同一个端口会失败,导致等待服务启动的逻辑无限重试挂起。 - 等待服务函数缩进错误:
_async_wait_for_server中try-except代码块没有缩进进while True循环内,实际执行时会卡在while True里无限创建socket,永远不会执行连接逻辑(单个测试能跑通大概率是你粘贴代码时缩进丢失,本地实际缩进是对的,但这个语法问题必须修正)。 - 服务销毁逻辑不全:你调用
cancel_handle.cancel()之后没有等待服务任务彻底退出,端口还被前一个服务占用时第二个测试就已经尝试启动新服务,必然绑定失败。 - 事件循环复用问题:你自定义的
event_loopfixture直接返回全局事件循环,没有做重置处理,多个测试用例复用同一个事件循环可能残留未处理的异步任务。
修复方案
1. 修正等待服务函数的缩进
async def _async_wait_for_server(event_loop, addr, port): while True: a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: await event_loop.sock_connect(a_socket, (addr, port)) return except ConnectionRefusedError: await asyncio.sleep(0.001) finally: a_socket.close()
2. 改用动态空闲端口+完善服务销毁逻辑
直接删除你自定义的event_loop fixture,用pytest-asyncio自带的即可,同时修改server fixture如下:
import socket @pytest.fixture() def server(event_loop): # 自动获取系统空闲端口,避免硬编码端口冲突 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', 0)) port = sock.getsockname()[1] sock.close() # 启动服务 cancel_handle = asyncio.ensure_future(main(port), loop=event_loop) event_loop.run_until_complete(asyncio.wait_for( _async_wait_for_server(event_loop, HOST, port), 5.0)) try: yield port finally: # 取消服务任务后等待彻底退出,释放端口 cancel_handle.cancel() try: event_loop.run_until_complete(cancel_handle) except asyncio.CancelledError: pass
3. 可选优化:服务启动时开启端口复用
如果要继续使用固定端口,可以在main函数启动服务时添加端口复用参数:
async def main(port): server = await asyncio.start_server( handle_echo, HOST, port, reuse_port=True, # Linux下生效 reuse_address=True # Windows、Linux都生效 ) addr = server.sockets[0].getsockname() print(f'SERVER: Serving on {addr[0:2]}') async with server: await server.serve_forever()
修改完成后两个测试用例就可以正常执行,不会出现挂起问题。
内容的提问来源于stack exchange,提问作者user3447843
相关产品推荐
相关产品推荐

