project_structure_documentation.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. from nicegui import ui
  2. from ..windows import bash_window, python_window
  3. from . import doc
  4. doc.text('Project Structure', '''
  5. The NiceGUI package provides a [pytest plugin](https://docs.pytest.org/en/stable/how-to/writing_plugins.html)
  6. which can be activated via `pytest_plugins = ['nicegui.testing.plugin']`.
  7. This makes specialized [fixtures](https://docs.pytest.org/en/stable/explanation/fixtures.html) available for testing your NiceGUI user interface.
  8. With the [`screen` fixture](/documentation/screen) you can run the tests through a headless browser (slow)
  9. and with the [`user` fixture](/documentation/user) fully simulated in Python (fast).
  10. If you only want one kind of test fixtures,
  11. you can also use the plugin `nicegui.testing.user_plugin` or `nicegui.testing.screen_plugin`.
  12. There are a multitude of ways to structure your project and tests.
  13. Here we only present two approaches which we found useful,
  14. one for [small apps and experiments](/documentation/project_structure#simple)
  15. and a [modular one for larger projects](/documentation/project_structure#modular).
  16. You can find more information in the [pytest documentation](https://docs.pytest.org/en/stable/contents.html).
  17. ''')
  18. doc.text('Simple', '''
  19. For small apps and experiments you can put the tests in a separate file,
  20. as we do in the examples
  21. [Chat App](https://github.com/zauberzeug/nicegui/tree/main/examples/chat_app)
  22. [Todo List](https://github.com/zauberzeug/nicegui/tree/main/examples/todo_list/) and
  23. [Authentication](https://github.com/zauberzeug/nicegui/tree/main/examples/authentication).
  24. To properly re-initialize your `main.py` in the tests,
  25. you place an empty `__init__.py` file next to your code to make it a package
  26. and use the `module_under_test` marker to automatically reload your main file for each test.
  27. Also don't forget the `pytest.ini` file
  28. to enable the [`asyncio_mode = auto`](/documentation/user#async_execution) option for the user fixture
  29. and make sure you properly guard the `ui.run()` call in your `main.py`
  30. to prevent the server from starting during the tests:
  31. ''')
  32. @doc.ui
  33. def simple_project_code():
  34. with ui.row(wrap=False).classes('gap-4 items-stretch'):
  35. with python_window(classes='w-[400px]'):
  36. ui.markdown('''
  37. ```python
  38. from nicegui import ui
  39. def hello() -> None:
  40. ui.notify('Hello World!')
  41. ui.button('Click me', on_click=hello)
  42. if __name__ in {'__main__', '__mp_main__'}:
  43. ui.run()
  44. ```
  45. ''')
  46. with python_window(classes='w-[400px]', title='test_app.py'):
  47. ui.markdown('''
  48. ```python
  49. import pytest
  50. from nicegui import ui
  51. from nicegui.testing import User
  52. from . import main
  53. pytest_plugins = ['nicegui.testing.user_plugin']
  54. @pytest.mark.module_under_test(main)
  55. async def test_click(user: User) -> None:
  56. await user.open('/')
  57. await user.should_see('Click me')
  58. user.find(ui.button).click()
  59. await user.should_see('Hello World!')
  60. ```
  61. ''')
  62. @doc.ui
  63. def simple_project_bash():
  64. with bash_window(classes='max-w-[820px] w-full h-42'):
  65. ui.markdown('''
  66. ```bash
  67. $ ls
  68. __init__.py main.py test_app.py pytest.ini
  69. $ pytest
  70. ==================== test session starts =====================
  71. test_app.py . [100%]
  72. ===================== 1 passed in 0.51 s ======================
  73. ```
  74. ''')
  75. doc.text('Modular', '''
  76. A more modular approach is to create a package for your code with an empty `__init__.py`
  77. and a separate `tests` folder for your tests.
  78. In your package a `startup.py` file can be used to register pages and do all necessary app initialization.
  79. The `main.py` at root level then only imports the startup routine and calls `ui.run()`.
  80. An empty `conftest.py` file in the root directory makes the package with its `startup` routine available to the tests.
  81. Also don't forget the `pytest.ini` file
  82. to enable the [`asyncio_mode = auto`](/documentation/user#async_execution) option for the user fixture.
  83. ''')
  84. @doc.ui
  85. def modular_project():
  86. with ui.row(wrap=False).classes('gap-4 items-stretch'):
  87. with python_window(classes='w-[400px]'):
  88. ui.markdown('''
  89. ```python
  90. from nicegui import ui, app
  91. from app.startup import startup
  92. app.on_startup(startup)
  93. ui.run()
  94. ```
  95. ''')
  96. with python_window(classes='w-[400px]', title='app/startup.py'):
  97. ui.markdown('''
  98. ```python
  99. from nicegui import ui
  100. def hello() -> None:
  101. ui.notify('Hello World!')
  102. def startup() -> None:
  103. @ui.page('/')
  104. def index():
  105. ui.button('Click me', on_click=hello)
  106. ```
  107. ''')
  108. with ui.row(wrap=False).classes('gap-4 items-stretch'):
  109. with python_window(classes='w-[400px]', title='tests/test_app.py'):
  110. ui.markdown('''
  111. ```python
  112. from nicegui import ui
  113. from nicegui.testing import User
  114. from app.startup import startup
  115. pytest_plugins = ['nicegui.testing.user_plugin']
  116. async def test_click(user: User) -> None:
  117. startup()
  118. await user.open('/')
  119. await user.should_see('Click me')
  120. user.find(ui.button).click()
  121. await user.should_see('Hello World!')
  122. ```
  123. ''')
  124. with bash_window(classes='w-[400px]'):
  125. ui.markdown('''
  126. ```bash
  127. $ tree
  128. .
  129. ├── main.py
  130. ├── pytest.ini
  131. ├── app
  132. │ ├── __init__.py
  133. │ └── startup.py
  134. └── tests
  135. ├── __init__.py
  136. ├── conftest.py
  137. └── test_app.py
  138. ```
  139. ''')
  140. doc.text('', '''
  141. You can also define your own fixtures in the `conftest.py` which call the `startup` routine.
  142. Pytest has some magic to automatically find and use this specialized fixture in your tests.
  143. This way you can keep your tests clean and simple.
  144. See the [pytests example](https://github.com/zauberzeug/nicegui/tree/main/examples/pytests)
  145. for a full demonstration of this setup.
  146. ''')
  147. @doc.ui
  148. def custom_user_fixture():
  149. with ui.row(wrap=False).classes('gap-4 items-stretch'):
  150. with python_window(classes='w-[400px]', title='tests/test_app.py'):
  151. ui.markdown('''
  152. ```python
  153. from nicegui import ui
  154. from nicegui.testing import User
  155. async def test_click(user: User) -> None:
  156. await user.open('/')
  157. await user.should_see('Click me')
  158. user.find(ui.button).click()
  159. await user.should_see('Hello World!')
  160. ```
  161. ''')
  162. with python_window(classes='w-[400px]', title='conftest.py'):
  163. ui.markdown('''
  164. ```python
  165. import pytest
  166. from nicegui.testing import User
  167. from app.startup import startup
  168. pytest_plugins = ['nicegui.testing.user_plugin']
  169. @pytest.fixture
  170. def user(user: User) -> User:
  171. startup()
  172. return user
  173. ```
  174. ''')