Skip to content

Commit

Permalink
Merge pull request #2 from StratoDem/1-initial
Browse files Browse the repository at this point in the history
1 initial
  • Loading branch information
mjclawar authored Mar 27, 2018
2 parents c303bcf + 0a8953c commit becc6eb
Show file tree
Hide file tree
Showing 13 changed files with 331 additions and 1 deletion.
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
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Created by .ignore support plugin (hsz.mobi)
### VirtualEnv template
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json

.idea/
*.egg-info/
*.whl
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CHANGELOG

## 1.0.0 - 2018-03-27
### Added
- Initial publication
88 changes: 87 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +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.

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 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`.

[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
26 changes: 26 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
StratoDem Analytics : setup
Principal Author(s) : Eric Linden
Secondary Author(s) :
Description :
Notes :
March 27, 2018
"""

from setuptools import setup

setup(
name='dash-snapshot-testing',
version='1.0.0',
author='Michael Clawar, Eric Linden',
author_email='tech@stratodem.com',
packages=['snapshot_test'],
license='(c) 2018 StratoDem Analytics. All rights reserved.',
description='Dash snapshot testing package',
url='https://github.com/StratoDem/dash-snapshot-testing',
install_requires=[
'dash>=0.19.0',
'plotly>=2.2.3',
])
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')
122 changes: 122 additions & 0 deletions snapshot_test/snapshot_test_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""
StratoDem Analytics : SnapshotTest
Principal Author(s) : Eric Linden
Secondary Author(s) :
Description :
Notes :
March 26, 2018
"""

import json
import os
import plotly.utils
import unittest

from dash.development.base_component import Component


__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.
If the component and the snapshot match, the test passes. If the specified file is not
found, it is created and the test passes. This test will only fail if the file already
exists, and the component-as-JSON does not match the contents of the file.
Parameters
----------
component: Component
The output of a Dash component that will be rendered to the page
file_id: str
A string ID used to distinguish the multiple JSON files that may be used as
part of a single component's test cases
Returns
-------
None
"""
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:
# 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:
"""
Builds and returns the path for the specific JSON file used in this test.
Parameters
----------
file_id: str
A string ID used to distinguish the multiple JSON files that may be used as
part of a single component's test cases
Returns
-------
A string containing the path to the file.
"""
assert isinstance(file_id, str)

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

@staticmethod
def __load_snapshot(filename: str) -> dict:
"""
Opens the JSON file at the specified location and returns its contents in dict form.
Parameters
----------
filename: str
The path to the JSON file
Returns
-------
A dict of the JSON file contents.
"""
assert isinstance(filename, str)

with open(filename, 'r') as f:
return json.load(f)

@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.
Returns
-------
A string containing the path of the snapshots directory.
"""
if cls.snapshots_dir is None:
directory = os.path.join(os.curdir, '__snapshots__')

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 cls.snapshots_dir

0 comments on commit becc6eb

Please sign in to comment.