project_structure_documentation.py 7.7 KB

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