このエントリでは、Behaveの非同期処理を試したメモを残します。

Behaveの非同期処理については、以下で紹介されています。

Testing asyncio Frameworks | Noteworthy in Version 1.2.6 | behave https://behave.readthedocs.io/en/stable/new_and_noteworthy_v1.2.6.html#testing-asyncio-frameworks

非同期処理には、以下の2種類があります。

  • Async-steps … ステップ内で非同期処理の開始~完了待ちを行う
  • Async-dispatch and async-collect … ステップ間を跨がり、非同期処理の開始~完了待ちを行う

Async-steps … ステップ内で非同期処理の開始~完了待ち

1つ目の非同期処理は、ステップ内での非同期処理の開始~完了待ちです。

これは単に、ステップ関数内でasync/awaitを使えるようになるという話です。

以下にコード例を示します。

# -- FILE: sample.feature
Feature: sample feature
    Scenario: sample scenario
        Given sample step
# -- FILE: steps/sample.py
from behave import *
from behave.api.async_step import async_run_until_complete
import asyncio

@step('sample step')
@async_run_until_complete
async def step_impl(context):
    tasks = []
    for _ in range(10):
        tasks.append(asyncio.create_task(asyncio.sleep(5)))
    await asyncio.gather(*tasks)

実行結果は次の通りです。
5秒待ち合わせを10タスク実施しているのに、5秒程度でテストが完了している事に注目。

$ behave sample.feature
Feature: sample feature # sample.feature:2

  Scenario: sample scenario  # sample.feature:3
    Given sample step        # steps/sample.py:6 5.006s

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
1 step passed, 0 failed, 0 skipped, 0 undefined
Took 0m5.006s

Async-dispatch and async-collect … ステップ間を跨がり、非同期処理の開始~完了待ち

2つ目は、ステップ間を跨がるケースです。

接続システムへの初期化やデータ登録などを行うGivenがずらっと並んでいるシナリオの場合など、
上手く使えば、テスト実行の所要時間短縮に役立てられるかと思います。

以下にコード例を示します。

# -- FILE: sample.feature
Feature: sample feature
    Scenario: sample scenario
        When run async process
        And run async process
        And run async process
        Then await async process
# -- FILE: steps/sample.py
from behave import *
from behave.api.async_step import async_run_until_complete
from behave.api.async_step import use_or_create_async_context
import asyncio

@step('run async process')
def step_impl(context):
    async_context = use_or_create_async_context(context, "async_context")
    async_context.tasks.append(async_context.loop.create_task(asyncio.sleep(5)))

@step('await async process')
@async_run_until_complete
async def step_impl(context):
    await asyncio.gather(*context.async_context.tasks)

実行結果は次の通りです。
run async processは0秒で完了し、await async processで5秒かかる事に注目。

$ behave sample.feature
Feature: sample feature # sample.feature:2

  Scenario: sample scenario  # sample.feature:3
    When run async process   # steps/sample.py:7 0.000s
    And run async process    # steps/sample.py:7 0.000s
    And run async process    # steps/sample.py:7 0.000s
    Then await async process # steps/sample.py:12 5.005s

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
4 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m5.006s

各非同期処理の結果(成功/失敗)も考慮

実際のテストシナリオでは、
Thenの待ち合わせの箇所で、各非同期処理が成功したか否かを判断する事になると思うので、
次のようなコードになると思います。

# -- FILE: sample.feature
Feature: sample feature
    Scenario: sample scenario
        When run async process ok
        And run async process ng
        Then await async process
# -- FILE: steps/sample.py
from behave import *
from behave.api.async_step import async_run_until_complete
from behave.api.async_step import use_or_create_async_context
import asyncio

@step('run async process ok')
def step_impl(context):
    async def async_ok():
        await asyncio.sleep(5)
        return True
    async_context = use_or_create_async_context(context, "async_context")
    async_context.tasks.append(async_context.loop.create_task(async_ok(), name="task_ok"))

@step('run async process ng')
def step_impl(context):
    async def async_ng():
        await asyncio.sleep(5)
        return False
    async_context = use_or_create_async_context(context, "async_context")
    async_context.tasks.append(async_context.loop.create_task(async_ng(), name="task_ng"))

@step('await async process')
@async_run_until_complete
async def step_impl(context):
    await asyncio.gather(*context.async_context.tasks)
    for e in context.async_context.tasks:
        assert e.result(), f'{e.get_name()} failed'

実行結果は次の通りで、task_ngの方が失敗したことがわかります。

$ behave sample.feature
Feature: sample feature # sample.feature:2

  Scenario: sample scenario   # sample.feature:3
    When run async process ok # steps/sample.py:7 0.000s
    And run async process ng  # steps/sample.py:15 0.000s
    Then await async process  # steps/sample.py:23 5.006s
      Assertion Failed: task_ng failed



Failing scenarios:
  sample.feature:3  sample scenario

0 features passed, 1 failed, 0 skipped
0 scenarios passed, 1 failed, 0 skipped
2 steps passed, 1 failed, 0 skipped, 0 undefined
Took 0m5.006s

以上。