-
-
Notifications
You must be signed in to change notification settings - Fork 863
Add possibility to perform tests without nicegui plugins and pytest configuration #5380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
evnchn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2 things:
- How the import is written, it doesn't match the codebase
- I am initially lost over the purpose of this PR. Now, I think this PR is doing, really, "testing without pytest", do you agree?
thanks
| async def run(ui_code): | ||
| """A user context manager for the given `nicegui.ui` code. | ||
|
|
||
| Example use in a plain pytest function without user plugin use: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm rethinking: The function is just a plain test function now, have very little to do with pytest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. You can use run(ui_code) in a pytest test function or with any other test library, or just in regular functions.
But it in the end, it enables you to run pytest tests without using the nicegui pytest plugins.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor issue in retrospect.
But still I think the main selling point for this PR is "test without pytest" because that is a superset of "pytest without plugins" and if we describe this PR by "pytest without plugins" we're underselling it.
Fully agreed, of course. I have copied the code from my implementation for the pyinstaller hook, which was not within the nicegui codebase. Many other things would need to be adapted, also. Like type hints, doc string, etc. |
The main purpose is not to "test without pytest" but to remove the need for the nicegui pytest plugins. |
evnchn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Function naming suggestions:
run_userormake_usercan help hammer down we're getting aUser - PR positioning: Though "pytest without plugins" was the intention, suggest keep "test without pytest" as how we communicate because that is the happy-accident and also a superset of the former.
- Code duplication: Suggest to refactor instead of copy the code twice.
| async def run(ui_code): | ||
| """A user context manager for the given `nicegui.ui` code. | ||
|
|
||
| Example use in a plain pytest function without user plugin use: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor issue in retrospect.
But still I think the main selling point for this PR is "test without pytest" because that is a superset of "pytest without plugins" and if we describe this PR by "pytest without plugins" we're underselling it.
| try: | ||
| # simulate user and keep NiceGUI fully headless for tests | ||
| os.environ['NICEGUI_USER_SIMULATION'] = 'true' | ||
|
|
||
| # don't spawn reloader/native window; don't open browser | ||
| ui.run(ui_code, reload=False, native=False, show=False) | ||
|
|
||
| async with core.app.router.lifespan_context(core.app): | ||
| async with httpx.AsyncClient( | ||
| transport=httpx.ASGITransport(core.app), | ||
| base_url='http://test' | ||
| ) as client: | ||
| yield User(client) | ||
| finally: | ||
| os.environ.pop('NICEGUI_USER_SIMULATION', None) | ||
| ui.navigate = Navigate() | ||
| ui.notify = notify | ||
| ui.download = download |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some logic is shared with nicegui/testing/user_plugin.py.
If that is the case, could we adhere to the DRY principle and better highlight the (I think quite high) similarity between the user fixture and this new function, either by:
- User fixture can simply call this new function?
- User fixture and this new function call some common function?
|
|
||
|
|
||
| @asynccontextmanager | ||
| async def run(ui_code): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run may cause confusion with ui.run. And also doesn't show the fact that this outputs a User which then unlocks all of https://nicegui.io/documentation/user
I am proposing get_user or make_user to hammer-down the fact that we're getting a User. Not sure if this name really works out though.
I like
I find "test without pytest" quite irritating because you still need to run your tests with
It might be good to do the import via |
|
Thank you a lot for your renewed feedback. In the meantime, I have come up with a slightly modified approach with even simpler user interface. You can now test code snippets without defining a ui_code callback function async def test_get_user_context():
async with get_user() as user:
ui.label('as')
await user.should_see('as')or access multiple pages directly in the context manager async def test_get_user_context_with_page_definitios():
async with get_user() as user:
@ui.page('/')
def page():
ui.label('Main page')
@ui.page('/a')
def pageA():
ui.label('Page A')
await user.open('/')
await user.should_see('Main page')
await user.open('/a')
await user.should_see('Page A')or access a main file by specifying its path async def test_get_user_context():
async with get_user(path_to_mainfile) as user:
await user.should_see('as')(Implementation is not yet committed to the PR branch.) What do you think? |
|
When necessary, ignore some of my previous comments to respect the majority vote. For async with run(some_code) as user:
...async with get_user() as user:
some_code()
...So I quite like
So possibly |
|
Yes, I like your "slightly modified approach" @himbeles where the context can also provides an enclosure for ui code. As @evnchn suggested the I just struggle with the naming.
|
Motivation
As discussed and proposed in #5377,
this adds a context manager
async run(ui_code) as user: ...within the NiceGUI testing framework.It enables testing of UI code without configuration of the NiceGUI plugin setup proposed in the documentation.
This can be necessary if NiceGUI is used in a setup where the pytest configuration can't be adapted.
As discussed in #5377, this was necessary for UI tests required within the pyinstaller plugin project.
It can also be helpful as another way to get started with testing in a new user's project -- without the need to configure pytest at all for NiceGUI, minimal API overhead in the call sites, and without hidden plugin "magic".
An example of a test function would be:
Implementation
The implementation relies on an async context manager which executes
ui.runand points an httpx.AsyncClient as aUserto it, in line with the setup and de-setup performed innicegui.testing.user_plugin.user. The context manager exposes theUserfor UI tests.Open points in the architecture would be:
run,user_runto not risk overloading the very simplerunlater,user_of, ...n_users>1, for example.I'm open for discussion and refine the implementation if you consider this useful.
Progress