async API test
aio-todoist 測完 api.py
之後包了一版上 PYPI,稍微記錄下 aiohttp 的 client 測試跟用 pytest 的 WTF 。
先講講 pytest ,之前寫 unittest 的時候微稍看過一下,覺得 configuration 太多了就沒詳細用(畢竟 builtin 的 unittest 在小測試的情況就很夠用了)。
這次在寫 test cases 的時候想說有時間就來看一下,順便裝個 pytest-watch continuous test 一下,省得 terminal 跟 source code 切來切去的各種麻煩。
(btw, sublime 可以裝 Terminus 就可以直接在 editor 裡面開 console 了, vscode 有富爸爸我記得原本就有整合了…)
一裝下光 execution 不一致的部份就研究了老半天,簡單來說用 python -m pytest
跟直接下 command pytest
兩個方式拿到的 sys.path
會不一樣,官方文件是這麼說的:
Running pytest with
pytest [...]
instead ofpython -m pytest [...]
yields nearly equivalent behaviour, except that the latter will add the current directory tosys.path
, which is standardpython
behavior.
好的好的,但不是說好直接 adopt builtin 的 unittest 嗎? unittest 的 behaviour 不管你怎麼執行都是後者呢…
簡單來說,如果文件結構像這樣:
root_dir
+-- tests
| +-- conftest.py
| +-- tests_abc.py # from THE_PROJECT import abc
+-- THE_PROJECT
| +-- miscellaneous modules
執行 python -m unittest
或 python -m pytest
可以正常跑測試,但直接執行 pytest
會跳 ImportError
。
好的好的,然後我就花了一堆時間研究 document 跟看了一下 stackoverflow 的 解法 …很好,直接把 parent dir 塞進 sys.path
裡面,簡單直接又暴力 — 但我不喜歡。
花了一堆時間測了各種 command line arguments ,最後測到直接在 tests 裡面塞個空的 __init__.py
就可以了… (((??????? MTFK???
至於為什麼不乾脆放棄 pytest
直接用 python -m pytest
來執行呢…當然是因為我主要的需求是 pytest-watch ,嗯是的我還跑去瞄了一下 code 。(但想想不對側室是無辜的…)
好的,接下來還是 pytest 的時間,這次是 log 的部份。
因為一直用 ptw
勾著跑測試就沒特別再試 unittest 跑出來的 output (想想畢竟都相容了…),直到東西丟上 pypi 之後想說用 unittest 跑看看結果怎麼樣…
一試不得了,到底哪來的 unhandled callback Exception 勒??為什麼 pytest 都沒東西?? 不是都吐到 log.error
了嗎??
好的,所以 pytest default 的 root log handlers 是:
(Pdb) asyncio.log.logger.root.handlers[<_LiveLoggingNullHandler (NOTSET)>, <_FileHandler /dev/null (NOTSET)>, <LogCaptureHandler (NOTSET)>, <LogCaptureHandler (NOTSET)>](Pdb) asyncio.log.logger.handlers[]
(所以我說到底為什麼一個測試用的 libary 要預設把 root log 吃掉???)
研究了好幾下 cli arguemtns 發現 --show-capture
是不行的呢, --debug
, — verbosity
, -v
或是 --log-level
都沒有,你得要用 --log-cli-level
才會有東西….
(所以我說到底為什麼預設的 root log handlers 要設計被黑洞吃掉呢到底???)
老實說比較直接開箱即用的 builtin unittest , pytest 的各種 configurations 跟 decorate fixtures 的設計對於新的使用者來說真的不太友善。
(只是寫幾個小測試卻得要先看滿滿的設定、API,那比起單一頁面找一下 self.assertXXX
也沒有好到哪去…)
回過頭來說說 aiohttp 測試的部份,library 內有提供 test_utils 也有文件 for unittest & pytest ,不過我個人建議是直接看 source code 比較好懂…畢竟文件寫的東西有點…不太齊? (pytest 用的 fixture 在這)
unittest 的部份, class 繼承 AioHTTPTestCase
、實作 get_application
,async 的測試要先 @unittest_run_loop
,大致上…就這樣。(???)
aio-todoist 在測 api.py
的部份主要是測 async 的兼容性(compatibility?),所以跟 server 之間往來的資料還是用 mock
比較多…
測 _get
dispatch to _get_async
的部份用了一下 AsyncMock
,不過 AsyncMock
拿到的 coroutine 沒辦法判斷 coro.close()
有沒有被呼叫,只能測 not awaited …
可能一般來說 coroutine spawn 之後就是直接被 schedule ( await
or ensure_future()
) 了才沒有實作(看了一下 mock 要弄也是蠻複雜的QQ),但實際上如果 coroutine spawn 之後沒有被 schedule 是會噴 log 的 (用不到的話記得用 .close()
關掉啊)
第二個比較有趣的部份是 future.add_done_callback(cb)
,callback function 在 future 完成 (set_result/set_exception/cancel
) 之後都會被呼叫,但如果在 callback 內發生 exception ,外部 (caller) 是抓不到的…一般來說會被 loop 的 exception handler 處理(預設是吐 traceback 給 log.error),相關的 API 可以參考文件。(((((所以說 pytest 把 log 直接吃掉真的不好!!!
api.commit()
在寫測試的時候才發現有 bug …因為是透過 api.sync()
來作,在 async scenario 下拿到的應該是 future
而不是一開始寫的 coroutine
,總之在 sync function 裡面拿到 future 還要再做 callback 真的有點賽(完整的部份可以看 github):
if self.queue:
queue = self.queue[:]
ret = self.sync(commands=queue)
self.queue[:] = []
if isfuture(ret):
src_fut = ret
ret = ensure_future(_helper(src_fut))
ret.add_done_callback(_check_cancel)
else:
_callback(ret)
_helper
是 async-function,當然也是可以用 sync-function 來實作兩個 futures 的 chaining 啦,但這樣的話勢必得利用 src_fut.add_done_callback()
再用 partial 把要 return 的 future 塞在一起,真心搞死自己XD
_helper
的實作方式就比較單純一點,在裡面 await 再處理 exceptions 的 self.queue
restore ,這樣就可以先轉成 coroutine 再轉成 future 傳出去,比較需要注意的是 future 是可以取消的,在這樣的情況下變成要透過 add_done_callback
去連著取消前一個 future 。
至於到底 network I/O 的 cancellation 到底有沒有用就是另一回事了…但我有稍微測過 todoist API 在 duplicate self.queue
資料操作的時候是沒有問題的(XD),所以就當作有用吧,發出去失敗的 commands 還是要塞回 self.queue
才對!!
另一個用 async _helper
的原因是 _callback
裡面會 raise Exception
,用 callback + partial 的方式還要再處理 exception ,寫出來的 code 真的醜….
雜七雜八時間!
pypi 有上傳專用的 api token 了!! 不知道什麼時候加的,但至少 Github Action 裡面不用再塞密碼了 XDD
唯一缺點是你要先上傳第一版,才可以申請那個 project 專用的 token ,然後目前好像用設定的 scope 還有限就是…
然後上一篇本來想要寫 async-generator (todoist 裡面 archive manager 的實作),但後來忘了…可能也許大概下次 ?