Skip to content

App.run_test() is Incompatible with PyTest Fixtures #4998

@Chris3606

Description

@Chris3606

Have you checked closed issues?

Yes

Issue

The App.run_test() function is incompatible with pytest fixtures. Consider the following code example:

import pytest
import pytest_asyncio
from textual.app import App
from textual.widgets import TextArea

class MyApp(App):
    BINDINGS = [('ctrl+a', 'add_widget', 'Add a widget.')]

    async def action_add_widget(self):
        await self.mount(TextArea())

@pytest_asyncio.fixture
async def my_pilot():
    app = MyApp()
    async with app.run_test() as pilot:
        yield pilot

@pytest.mark.asyncio
async def test_add_widget_programatically(my_pilot):
    await my_pilot.app.action_add_widget()

if __name__ == '__main__':
    app = MyApp()
    app.run()

Running this textual run works fine; you can press ctrl + a and the TextArea widget appears. However, if you runpytest, the test fails because calling the action_add_widget() function raised NoActiveAppError.

Environment:

textual==0.79.1
textual-dev==1.6.1
textual-serve==1.1.1
pytest==8.3.3
pytest-asyncio==0.24.0

I discussed this on a discord, and presumably there is a somewhat known incompatibility with pytest fixtures here. Moving the async with app.run_test() as pilot: line to within the test instead of using a fixture works fine for this case. However, I consider this a bug for a number of reasons.

First, not using fixtures is impractical for some use cases. The inability to use fixtures, for example, precludes the ability to use a single app instance to run multiple tests. A practical use case where this might be useful, is consider a CLI-like FTP client. One might want to connect to an FTP server, run a number of commands on the same server as separate test, and then disconnect (eg. session fixture scope).

Additionally, this issue has a code smell that makes me suspect undefined behavior/race conditions. A number of workarounds all work to solve the issue for this use case, including:

  • Moving the async_with statement to inside the test
  • Calling the action by simulating the keypress instead of calling the function
  • Adding await asyncio.sleep(0) to the fixture right before the yield

However, not all of these workarounds work for all use cases; in fact the only one that has thus far proven reliable is moving the async with statement (and my sample size is still relatively small). It seems to me to be in part a timing issue, thus the concern that the workaround is concealing a bigger issue.

Textual Diagnose Output

As far as I'm aware, since this only occurs with pytest, I can't do this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions