Usage

Install

Install with pip:

pip install -U pytest-dash

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_threaded()

dash_subprocess 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')
    ...

Selenium wait for wrappers

The wait_for module is especially useful if you need to interact with elements or props that are only loaded after a callback as there might be a delay between when the callback is handled and returned to the frontend.

Available wrappers:

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 $, (eg: $value).

application:
path:

Dot notation path to the application file.

options:
port:The port used by the application.
event:

List of actions to execute.

outcome:

List of expected result of the scenario event.

Commented example
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.
Scenario event/outcome syntax
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:
simple_app.py
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:
test_simple_callback.yml
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

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 should assert something inside the function.
  • inline/tree/meta Only one can to be set to true, default is inline, decorate the function with lark.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)

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
Run chrome in headless mode.
from selenium.webdriver.chrome.options import Options


def pytest_setup_selenium(driver_name):
    options = Options()
    options.headless = True
    return {
        'chrome_options': options,
    }