6

I am trying to write a testcase for a Dash callback function in a page which store selected rows of 2 datatables into dash_core_components.Store, and determine the state by dash.callback_context

Sample callback function in index.py

@app.callback(
    Output('rows-store', 'data'),
    Input('datatable1', 'selected_rows'),
    Input('datatable2', 'selected_rows'),
)
def store_row_selection(row1: list, row2: list) -> dict:
    ctx = dash.callback_context
    if not ctx.triggered:
        return {'row-index-v1': [0], 'row-index-v2': [0]}
    else:
        return {'row-index-v1': row1, 'row-index-v2': row2}

Sample testcase in test.py

def test_store_row_selection_1(app_datatable):
    r1 = [0]
    r2 = [1]
    result = store_row_selection(r1, r2)

    assert type(result) is dict  

However, Pytest throws an exception when running, what is the proper way to test a callback function and how can I get this to work?

  @wraps(func)
    def add_context(*args, **kwargs):
>       output_spec = kwargs.pop("outputs_list")
E       KeyError: 'outputs_list'
Kit Law
  • 323
  • 6
  • 15

1 Answers1

8

I recommend you to test the function undecorated by @app.callback.

Dash uses functools.wraps for creating callbacks (source code). This means we have access to the __wrapped__ attribute on the store_row_selection function.

The __wrapped__ attribute gives us the original underlying function, store_row_selection in this case:

def test_store_row_selection_1():
    r1 = [0]
    r2 = [1]
    result = store_row_selection.__wrapped__(r1, r2)

    assert type(result) is dict

Another approach would be to split your callback code up into parts:

def do_stuff(row1, row2):
    # Do things
    return {"row-index-v1": row1, "row-index-v2": row2}


@app.callback(
    Output("rows-store", "data"),
    Input("datatable1", "selected_rows"),
    Input("datatable2", "selected_rows"),
)
def store_row_selection(row1: list, row2: list) -> dict:
    return do_stuff(row1, row2)

and test the function which is not dependent on Dash callbacks (do_stuff in this example).

Update based on edit and comment

May I know if it is possible to trigger ctx.triggered in pytest as well?

dash.call_context is only available inside a callback (source), so the __wrapped__ approach wouldn't work in that case. The other approach (splitting up the function) would work as you could pass ctx to do_stuff and simulate callback_context in your tests.

5eb
  • 14,798
  • 5
  • 21
  • 65
  • Thanks! May I know if it is possible to trigger ctx.triggered in pytest as well? (Please see the updates in my question) – Kit Law May 25 '21 at 10:56
  • 1
    @KitLaw I've updated my answer. I don't really know of a better way, but maybe you could mock `dash.callback_context` https://docs.python.org/3/library/unittest.mock-examples.html – 5eb May 25 '21 at 14:21