视频原文:Strategies for testing Async code - PyCon 2019
同时参考了:
Testing Asyncio Python Code with Pytest
前面几篇关于异步编程的文章:
异步编程 101: 是什么、小试Python asyncio
异步编程 101:Python async await发展简史
异步编程 101:写一个事件循环
异步编程 101:asyncio中的 for 循环
异步编程 101:asyncio 进阶上篇
异步编程,本质上是通过合作(cooperation)来达成并发效果,也即:需要 wait 的时候,也就是发生 IO 的时候,把控制权交给主事件循环。 (yield control when 'awaiting' asynchronous results.) 这个过程有点像事件循环完成了操作系统的工作,可以将事件循环看作是操作系统,然后把协程看作是线程这么来理解。整个事件循环是在一个线程里面的,意味着任务切换更加高效,无需上下文转换。
异步代码很高效,但是也有很蛋疼的地方,那就是测试。
0x01 : async 测试实例
来通过一个简单的例子看一下吧:一个Cat
类,有一个 move
方法,这个方法是异步的。
![640?wx_fmt=png](https://img-blog.csdnimg.cn/img_convert/5be0dc9e8a73b323268a5e09cdc7908a.png)
然后用 unittest 写一个测试类,你能发现下面代码的问题吗?
![640?wx_fmt=png](https://img-blog.csdnimg.cn/img_convert/19b2db925000212c426e98d6ef6d5db4.png)
herd(grafield, 'forward')
返回的是一个协程对象(coroutine object),如果你不await
他,什么也不会发生。而coroutine object是 truthy 的,所以assertTrue()
是能够通过的。如果你运行一下 test,会看到coroutine herd was nerver awaited
的 warning。
![640?wx_fmt=png](https://img-blog.csdnimg.cn/img_convert/a4ae279b8448613f79bfbc4af2d6b5c1.png)
下图这样调用await
还是不对的,因为await
关键字只能出现在async
函数里面。
![640?wx_fmt=png](https://img-blog.csdnimg.cn/img_convert/bdc54b4bf4c20c20093a12fa597c2087.png)
一个解决方案是加入事件循环:
![640?wx_fmt=png](https://img-blog.csdnimg.cn/img_convert/e5a2e7286d328001e82e513d7954a01c.png)
这能work,但是估计你也看出来了,这很麻烦。如果我有多个方法,难道我需要每个 test
方法都加一个事件循环吗?更重要的是,我只是想做一下单元测试,事件循环在这个时候实际上是一个底层细节,我不需要关心。
在 Python3.7 中,asyncio新增了一个方法:asyncio.run()
,为你隐藏了事件循环的细节,所以能够让代码更加简洁:
![640?wx_fmt=png](https://img-blog.csdnimg.cn/img_convert/9bf1999182a012862fa28eb9e0e09fc5.png)
0x02 pytest-asyncio
安装:pip install pytest-asyncio
,这实际上是pytest
的一个插件。
![640?wx_fmt=png](https://img-blog.csdnimg.cn/img_convert/1b7ba544a2756f46dd4c2850aa10114a.png)
用法很简单,重要的是我们得知道工作原理。之前的代码问题在于,pytest 默认的的 runner 会将所有的函数当作普通函数处理,而对于 async 函数, 调用的时候返回的是一个 coroutine object。所以我们得想办法告诉 pytest 使用一个 eventloop 来运行测试方法。
一种方法是,实例化一个eventloop 然后注入到 tests里面,比如:
import asyncio
import pytest
async def say(what, when):
await asyncio.sleep(when)
return what
@pytest.fixture
def event_loop():
loop = asyncio.get_event_loop()
yield loop
loop.close()
def test_say(event_loop):
expected = 'This should fail!'
assert expected == event_loop.run_until_complete(say('Hello!', 0))
这种方法不方便之处在于每次都需要手动注入 eventloop,更优雅的方法是调整 test runner,让它识别 async 函数,当作 asyncio tasks 来执行。
pytest-asyncio
完成的功能就是这样的,它的 API 非常简单,你只需要为 async function 添加一个 @pytest.mark.asyncio
修饰器即可:
import pytest
from say import say
@pytest.mark.asyncio
async def test_say():
assert 'Hello!' == await say('Hello!', 0)