Usage¶
Write integration tests¶
pytest-dash
provides fixtures and helper functions
to write Dash integration tests.
To start a Dash instance, you can use a dash_threaded
or
dash_subprocess
fixture.
The fixture will start the server when called and wait until the application has been loaded by the browser. The server will be automatically closed in the test teardown.
dash_threaded example¶
Start a dash application in a thread, close the server in teardown.
In this example we count the number of times a callback method is called
each time a clicked is called and assert the text output of a callback
by using wait_for_text_to_equal()
.
import dash
from dash.dependencies import Output, Input
from dash.exceptions import PreventUpdate
import dash_html_components as html
from pytest_dash import wait_for
def test_application(dash_threaded):
# The selenium driver is available on the fixture.
driver = dash_threaded.driver
app = dash.Dash(__name__)
counts = {'clicks': 0}
app.layout = html.Div([
html.Div('My test layout', id='out'),
html.Button('click me', id='click-me')
])
@app.callback(
Output('out', 'children'),
[Input('click-me', 'n_clicks')]
)
def on_click(n_clicks):
if n_clicks is None:
raise PreventUpdate
counts['clicks'] += 1
return 'Clicked: {}'.format(n_clicks)
dash_threaded(app)
btn = wait_for.wait_for_element_by_css_selector(driver, '#click-me')
btn.click()
wait_for.wait_for_text_to_equal(driver, '#out', 'Clicked: 1')
assert counts['clicks'] == 1
dash_subprocess example¶
Start the server in subprocess with waitress-serve
.
Kill the process in teardown.
from pytest_dash.wait_for import wait_for_text_to_equal
def test_subprocess(dash_subprocess):
driver = dash_subprocess.driver
dash_subprocess('test_apps.simple_app')
value_input = driver.find_element_by_id('value')
value_input.clear()
value_input.send_keys('Hello dash subprocess')
wait_for_text_to_equal(driver, '#out', 'Hello dash subprocess')
Note
This fixture is slower than threaded due to the process spawning.
See also
Fixtures: | dash_threaded dash_subprocess |
---|
Helpers¶
Importing applications¶
Import existing Dash applications from a file with
import_app()
.
The application must be named app.
Example: |
---|
from pytest_dash.application_runners import import_app
def test_application(dash_threaded):
app = import_app('my_app')
...
Write declarative scenario tests¶
Pytest-dash include a declarative way to generate tests in a yaml format.
When pytest finds yaml files prefixed with test_
in a directory, it will
generate tests from a Tests
object.
Schema¶
A yaml test file contains scenario definitions and a list of parametrized of scenarios to execute.
Globals¶
application: | Global default application to use in the tests if no option supplied. |
---|---|
Tests: | List of scenario to generate tests for. Test item props are used as parameter. |
Scenario object¶
parameters: | Object where the keys will be used to create a variable dictionary to
use in behavior commands. Use a parameter in commands by prefixing the key
with |
||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
application: |
|
Scenario: # Key of the test
parameters: # Optional values to use in test
value:
default: 4
application: # The application settings to use in the test
path: test_apps.simple_app # Dot notation path to the app file.
options: # Application options such as port
port: 8051
event: # List of actions describing what happen.
- "enter $value in #input"
outcome: # The expected result of the event.
- "text in #output should be $value"
Tests: # List of all the scenarios to execute.
- Scenario # Runs Scenario with the default parameter.
- Scenario
value: 8 # Override the default parameter.
Syntax¶
There is 3 kind of rule for the grammar:
value
, return a value.command
, execute an action.comparison
, compare two value.
Rule | Kind | Example | Description |
---|---|---|---|
element_id | value | #my-element-id |
Find a single element by id |
element_selector | value | {#my-element-id > span} |
Find a single by selector |
elements_selector | value | *{#my-element-id > span} |
Find multiple elements by selector, actions will be executed on all elements (Currently click & length assertions) |
element_xpath | value | [//*[@id="btn-1"]] |
Find a single element by xpath |
elements_xpath | value | *[//div[@id="container"]/span] |
Find multiple elements by xpath. |
element_prop | value | #my-input.value |
A property of an element to use in comparisons. |
eq | comparison | #my-input.value should be 1 , #my-input.value == 1 |
Equality comparison |
lt | comparison | #my-input.value < 3 , #my-input.value should be less than 3 |
The value should be less than. |
lte | comparison | #my-input.value <= 3 ,``#my-input.value should be less or equal than 3`` |
The value on the left should be less or equal to. |
gt | comparison | #my-input.value > 3 , #my-input.value should be greater than 3 |
Value should be greater. |
gte | comparison | #my-input.value >= 3 , #my-input.value should be greater or equal than 3 |
Greater or equal comparison. |
text_equal | comparison | text in #output should be "Foo bar" |
Special comparison for text attribute, it uses the wait_for api. |
prop_compare | comparison | #output.value should be 3 |
Property comparison uses the wait_for api |
style_compare | comparison | style "padding" of #btn should be "3px" |
wait_for comparison for a style attribute of an element. |
clear | command | clear #my-input |
Clear the value of an element. |
click | command | click #my-btn |
Click on an element, the element must be visible to be clickable. |
send_value | command | enter "Foo bar" in #my-input |
Send keyboard input to an element. |
Note
The syntax can be extended with Hooks.
Examples¶
Application: |
---|
import dash
from dash.dependencies import Output, Input
from dash.exceptions import PreventUpdate
import dash_html_components as html
import dash_core_components as dcc
app = dash.Dash(__name__)
app.layout = html.Div([
dcc.Input(id='value', placeholder='my-value'),
html.Div(['You entered: ', html.Span(id='out')]),
html.Button('style-btn', id='style-btn'),
html.Div('style-container', id='style-output'),
])
@app.callback(Output('out', 'children'), [Input('value', 'value')])
def on_value(value):
if value is None:
raise PreventUpdate
Test: |
---|
SimpleCallback:
description: Test a dcc.Input callback output to a html.Div when a html.Button is clicked\
parameters:
var1:
description: Value to send to the input
type: str
default: hello world
application:
path: test_apps.simple_app
port: 8051
event:
- "clear #value"
- "enter $var1 in #value"
outcome:
- "#value.value == $var1"
- 'text in {#out} should be $var1'
Tests:
- SimpleCallback
- SimpleCallback:
var1: foo bar
See also
Run tests¶
Use $ pytest tests --webdriver Chrome
to run all the test
The --webdriver
option is used for choosing the selenium driver to use.
Choose from:
Note
The driver must be available on your environment PATH.
See also
Please refer to https://selenium-python.readthedocs.io/installation.html for selenium installation.
Configuration¶
The default webdriver for a project can be specified in pytest.ini instead of having to enter it on the command line every time you run a test.
Example: | ./pytest.ini |
---|
[pytest]
webdriver = Chrome
Hooks¶
pytest_add_behaviors¶
The scenario event/outcome syntax can be extended with
the pytest_add_behaviors()
hook.
add_behavior
is a decorator with the following keywords arguments:
syntax
The syntax to match, it will be available under the name of the function in the parser.kind
value
default, A value can be used in commands and comparisons.command
, Complete custom line parsing.comparison
, A comparison shouldassert
something inside the function.
inline/tree/meta
Only one can to be set to true, default is inline, decorate the function withlark.v_args(inline=inline, tree=tree, meta=meta)
, lark.v_args docs.
Example: | tests/conftest.py |
---|
def pytest_add_behaviors(add_behavior):
@add_behavior('"eval("/.*/")"')
def evaluate(command):
return eval(command)
See also
Lark grammar reference https://lark-parser.readthedocs.io/en/latest/grammar/
pytest_setup_selenium¶
If you need to configure the selenium driver used by the plugin, you can use
the pytest_setup_selenium
hook.
Example: | tests/conftest.py |
---|
from selenium.webdriver.chrome.options import Options
def pytest_setup_selenium(driver_name):
options = Options()
options.headless = True
return {
'chrome_options': options,
}