Skip to content

Commit

Permalink
Update documentation and add example snapshot tests. Rearrange files …
Browse files Browse the repository at this point in the history
…to have a snapshot_test directory that exports a DashSnapshotTestCase
  • Loading branch information
mjclawar committed Mar 27, 2018
1 parent fedd5ba commit 0a8953c
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 24 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
charset = utf-8

[*.py]
max_line_length = 100
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ pyvenv.cfg
.venv
pip-selfcheck.json

.idea/
*.egg-info/
*.whl
89 changes: 83 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,88 @@
# dash-snapshot-testing
Use snapshot testing to test Dash components
Use snapshot testing, inspired by Jest snapshot testing, to test [Dash][] components.

## Inspiration
Testing a long HTML component output for a Dash application is difficult.
It typically requires hardcoding data or setting up a dummy database.
Using snapshot tests that JSON serialize the Dash component output provide another
easy testing layer to ensure that code refactors/changes do not change the
output unexpectedly.

## Example
To learn more about snapshot testing in general, see a much more elaborate explanation from the [Facebook Jest site](https://facebook.github.io/jest/docs/en/snapshot-testing.html)

## Usage
```python
import dash_html_components as html

from snapshot_test import DashSnapshotTestCase


class MyUnitTestCase(DashSnapshotTestCase):
def test_component(self):
my_component = html.Div([html.P('wow'), html.Span('this works')], id='test-id')

self.assertSnapshotEqual(my_component, 'my-test-unique-id')
```

This outputs/checks this JSON at `__snapshots__/MyUnitTestCase-my-test-unique-id.json`:
```json
{
"type": "Div",
"props": {
"id": "test-id",
"children": [
{
"type": "P",
"props": {"children": "wow"},
"namespace": "dash_html_components"
},
{
"type": "Span",
"props": {"children": "this works"},
"namespace": "dash_html_components"
}
]
},
"namespace": "dash_html_components"
}
```

### Setting a custom `snapshots_dir` for the class
```python
class MyComponentNameTest(DashSnapshotUnitTest):
def test_func(self) -> None:
class MyOtherUnitTestCase(DashSnapshotTestCase):
snapshots_dir = '__snapshots_2__'

def test_component(self):
my_component = html.Div([html.P('wow'), html.Span('another one')], id='test-id')

self.assertSnapshotEqual(my_component, 'my-test-unique-id')
```

This outputs/checks this JSON at `__snapshots_2__/MyOtherUnitTestCase-my-test-unique-id.json`:
```json
{
"type": "Div",
"props": {
"id": "test-id",
"children": [
{
"type": "P",
"props": {"children": "wow"},
"namespace": "dash_html_components"
},
{
"type": "Span",
"props": {"children": "another one"},
"namespace": "dash_html_components"
}
]
},
"namespace": "dash_html_components"
}
```

At its core, this `unittest.TestCase` compares a JSON-serialized Dash component
against a previously stored JSON-serialized Dash component, and checks if the `dict`
objects from `json.loads` are equivalent using `assertEqual`.

self.assertSnapshotEqual()
```
[Dash]: https://github.com/plotly/dash
1 change: 1 addition & 0 deletions __snapshots_2__/MyOtherUnitTestCase-my-test-unique-id.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type": "Div", "props": {"id": "test-id", "children": [{"type": "P", "props": {"children": "wow"}, "namespace": "dash_html_components"}, {"type": "Span", "props": {"children": "another one"}, "namespace": "dash_html_components"}]}, "namespace": "dash_html_components"}
1 change: 1 addition & 0 deletions __snapshots__/MyUnitTestCase-my-test-unique-id.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type": "Div", "props": {"id": "test-id", "children": [{"type": "P", "props": {"children": "wow"}, "namespace": "dash_html_components"}, {"type": "Span", "props": {"children": "this works"}, "namespace": "dash_html_components"}]}, "namespace": "dash_html_components"}
4 changes: 4 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-r ./requirements.txt

dash-html-components
dash-renderer
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dash>=0.19
plotly>=2.2.3
8 changes: 3 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
StratoDem Analytics : setup
Principal Author(s) : Eric Linden
Secondary Author(s) :
Secondary Author(s) :
Description :
Notes :
Notes :
March 27, 2018
"""
Expand All @@ -16,9 +16,7 @@
version='1.0.0',
author='Michael Clawar, Eric Linden',
author_email='tech@stratodem.com',
packages=[
'SnapshotTest.DashSnapshotUnitTest',
],
packages=['snapshot_test'],
license='(c) 2018 StratoDem Analytics. All rights reserved.',
description='Dash snapshot testing package',
url='https://github.com/StratoDem/dash-snapshot-testing',
Expand Down
12 changes: 12 additions & 0 deletions snapshot_test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
StratoDem Analytics : __init__.py
Principal Author(s) : Michael Clawar
Secondary Author(s) :
Description :
Notes :
March 27, 2018
"""

from .snapshot_test_case import *
10 changes: 10 additions & 0 deletions snapshot_test/__tests__/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
StratoDem Analytics : __init__.py
Principal Author(s) : Michael Clawar
Secondary Author(s) :
Description :
Notes :
March 27, 2018
"""
31 changes: 31 additions & 0 deletions snapshot_test/__tests__/test_snapshot_test_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
StratoDem Analytics : __test_snapshot_test_case
Principal Author(s) : Michael Clawar
Secondary Author(s) :
Description :
Notes :
March 27, 2018
"""


import dash_html_components as html

from snapshot_test import DashSnapshotTestCase


class MyUnitTestCase(DashSnapshotTestCase):
def test_component(self):
my_component = html.Div([html.P('wow'), html.Span('this works')], id='test-id')

self.assertSnapshotEqual(my_component, 'my-test-unique-id')


class MyOtherUnitTestCase(DashSnapshotTestCase):
snapshots_dir = '__snapshots_2__'

def test_component(self):
my_component = html.Div([html.P('wow'), html.Span('another one')], id='test-id')

self.assertSnapshotEqual(my_component, 'my-test-unique-id')
41 changes: 28 additions & 13 deletions SnapshotTest.py → snapshot_test/snapshot_test_case.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
StratoDem Analytics : SnapshotTest
Principal Author(s) : Eric Linden
Secondary Author(s) :
Secondary Author(s) :
Description :
Notes :
Notes :
March 26, 2018
"""
Expand All @@ -17,7 +17,12 @@
from dash.development.base_component import Component


class DashSnapshotUnitTest(unittest.TestCase):
__all__ = ['DashSnapshotTestCase']


class DashSnapshotTestCase(unittest.TestCase):
snapshots_dir = None

def assertSnapshotEqual(self, component: Component, file_id: str) -> None:
"""
Tests the supplied component against the specified JSON file snapshot, if it exists.
Expand All @@ -37,19 +42,21 @@ def assertSnapshotEqual(self, component: Component, file_id: str) -> None:
-------
None
"""
assert isinstance(component, Component)
assert isinstance(file_id, str)
assert isinstance(component, Component), 'Component passed in must be Dash Component'
assert isinstance(file_id, str), 'must pass in a file id to use as unique file ID'

filename = self.__get_filename(file_id=file_id)

component_json = component.to_plotly_json()

if os.path.exists(filename):
# Load a dumped JSON for the passed-in component, to ensure matches standard format
expected_dict = json.loads(
json.dumps(component_json, cls=plotly.utils.PlotlyJSONEncoder))
self.assertEqual(self.__load_snapshot(filename=filename), expected_dict)
else:
with open(filename, 'w+') as file:
# Component did not already exist, so we'll write to the file
with open(filename, 'w') as file:
json.dump(component_json, file, cls=plotly.utils.PlotlyJSONEncoder)

def __get_filename(self, file_id: str) -> str:
Expand All @@ -68,7 +75,9 @@ def __get_filename(self, file_id: str) -> str:
"""
assert isinstance(file_id, str)

return os.path.join(self.__get_snapshots_dir(), '{}-{}.json'.format(self.__name__, file_id))
return os.path.join(
self.__get_snapshots_dir(),
'{}-{}.json'.format(self.__class__.__name__, file_id))

@staticmethod
def __load_snapshot(filename: str) -> dict:
Expand All @@ -89,8 +98,8 @@ def __load_snapshot(filename: str) -> dict:
with open(filename, 'r') as f:
return json.load(f)

@staticmethod
def __get_snapshots_dir() -> str:
@classmethod
def __get_snapshots_dir(cls) -> str:
"""
Checks for the existence of the snapshots directory, and creates it if it is not found.
It then returns the directory path.
Expand All @@ -99,9 +108,15 @@ def __get_snapshots_dir() -> str:
-------
A string containing the path of the snapshots directory.
"""
directory = os.path.join(os.curdir, 'snapshots')
if cls.snapshots_dir is None:
directory = os.path.join(os.curdir, '__snapshots__')

if not os.path.exists(directory):
os.mkdir(directory)

if not os.path.exists(directory):
os.mkdir(directory)
return directory
else:
if not os.path.exists(cls.snapshots_dir):
os.mkdir(cls.snapshots_dir)

return directory
return cls.snapshots_dir

0 comments on commit 0a8953c

Please sign in to comment.