Make組ブログ

Python、Webアプリや製品・サービス開発についてhirokikyが書きます。

asyncioとpytestでテスト用fixtureを作るときにgot Future <Future pending> attached to a different loopが出る話

Pythonでasyncioを使うアプリケーションのテストを書くとき、event loopに気を遣ってあげないとエラーになるという話です (今回はaiohttpを使ってWebアプリを作っているときに起こりました)。

こんなエラーがでました。

RuntimeError: Task <Task pending coro=<...> cb=[...]> got Future <Future pending> attached to a different loop

テスト側で動かすイベントループと、アプリ側が動かそうとする、イベントループに違いがあると発生してしまいます。 例えばアプリ側で何らかのバックエンドにアクセスするクライアントを使っていて、テスト側でもデータ(フィクスチャー)を作ったりクリーンアップしたいとします。 そのときに双方の使うクライアントが内部で使っているイベントループが違うものだと上記のエラーが発生します。

テスト用の pytest-asyncio などのライブラリーが、テスト用のイベントループを用意する場合は、アプリ側でクライアントを起動時にモジュールグローバルに保存しないようにしておきます。 そのクライアントがインスタンス化されるときに、内部で asyncio.get_event_loop を呼ぶことがあるからです。 もしくはテスト時にクライアントを差し替えるようにします。

この例だと pytest-asyncioevent_loop フィクスチャーを取っておくことでテスト用のイベントループを有効にしておきます。

import pytest

import somebackend


@pytest.fixture
async def somebackend(event_loop):
    client = somebackend.Client()
    try:
        yield client
    finally:
        do_cleanup_here()

テストでは以下のように使えます。

@pytest.mark.asyncio
async def test_foo(somebackend):
    await somebackend.create("some thing")

    actual = await call_function_under_test()

    assert actual == "expected"

今回は aiodocker を使っていて起こりました。

この情報が何かの参考になれば嬉しいです。 また、asyncioやイベントループ周りについて詳しければ教えてください。

近そうな話

github.com