-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Vendor the snapshottest repo rather than pull from github.
Works around new build issue.
- Loading branch information
1 parent
cf1fd3f
commit 53857fa
Showing
21 changed files
with
1,249 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2017-Present Syrus Akbary | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# SnapshotTest [![travis][travis-image]][travis-url] [![pypi][pypi-image]][pypi-url] | ||
|
||
[travis-image]: https://img.shields.io/travis/syrusakbary/snapshottest.svg?style=flat | ||
[travis-url]: https://travis-ci.org/syrusakbary/snapshottest | ||
[pypi-image]: https://img.shields.io/pypi/v/snapshottest.svg?style=flat | ||
[pypi-url]: https://pypi.python.org/pypi/snapshottest | ||
|
||
|
||
Snapshot testing is a way to test your APIs without writing actual test cases. | ||
|
||
1. A snapshot is a single state of your API, saved in a file. | ||
2. You have a set of snapshots for your API endpoints. | ||
3. Once you add a new feature, you can generate *automatically* new snapshots for the updated API. | ||
|
||
## Installation | ||
|
||
$ pip install snapshottest | ||
|
||
|
||
## Usage with unittest/nose | ||
|
||
```python | ||
from snapshottest import TestCase | ||
|
||
class APITestCase(TestCase): | ||
def test_api_me(self): | ||
"""Testing the API for /me""" | ||
my_api_response = api.client.get('/me') | ||
self.assertMatchSnapshot(my_api_response) | ||
|
||
# Set custom snapshot name: `gpg_response` | ||
my_gpg_response = api.client.get('/me?gpg_key') | ||
self.assertMatchSnapshot(my_gpg_response, 'gpg_response') | ||
``` | ||
|
||
If you want to update the snapshots automatically you can use the `nosetests --snapshot-update`. | ||
|
||
Check the [Unittest example](https://github.com/syrusakbary/snapshottest/tree/master/examples/unittest). | ||
|
||
## Usage with pytest | ||
|
||
```python | ||
def test_mything(snapshot): | ||
"""Testing the API for /me""" | ||
my_api_response = api.client.get('/me') | ||
snapshot.assert_match(my_api_response) | ||
|
||
# Set custom snapshot name: `gpg_response` | ||
my_gpg_response = api.client.get('/me?gpg_key') | ||
snapshot.assert_match(my_gpg_response, 'gpg_response') | ||
``` | ||
|
||
If you want to update the snapshots automatically you can use the `--snapshot-update` config. | ||
|
||
Check the [Pytest example](https://github.com/syrusakbary/snapshottest/tree/master/examples/pytest). | ||
|
||
## Usage with django | ||
Add to your settings: | ||
```python | ||
TEST_RUNNER = 'snapshottest.django.TestRunner' | ||
``` | ||
To create your snapshottest: | ||
```python | ||
from snapshottest.django import TestCase | ||
|
||
class APITestCase(TestCase): | ||
def test_api_me(self): | ||
"""Testing the API for /me""" | ||
my_api_response = api.client.get('/me') | ||
self.assertMatchSnapshot(my_api_response) | ||
``` | ||
If you want to update the snapshots automatically you can use the `python manage.py test --snapshot-update`. | ||
Check the [Django example](https://github.com/syrusakbary/snapshottest/tree/master/examples/django_project). | ||
|
||
## Disabling terminal colors | ||
|
||
Set the environment variable `ANSI_COLORS_DISABLED` (to any value), e.g. | ||
|
||
ANSI_COLORS_DISABLED=1 pytest | ||
|
||
|
||
# Contributing | ||
|
||
After cloning this repo and configuring a virtualenv for snapshottest (optional, but highly recommended), ensure dependencies are installed by running: | ||
|
||
```sh | ||
make develop | ||
``` | ||
|
||
After developing, ensure your code is formatted properly by running: | ||
|
||
```sh | ||
make format-fix | ||
``` | ||
|
||
and then run the full test suite with: | ||
|
||
```sh | ||
make lint | ||
# and | ||
make test | ||
``` | ||
|
||
To test locally on all supported Python versions, you can use | ||
[tox](https://tox.readthedocs.io/): | ||
|
||
```sh | ||
pip install tox # (if you haven't before) | ||
tox | ||
``` | ||
|
||
# Notes | ||
|
||
This package is heavily inspired in [jest snapshot testing](https://facebook.github.io/jest/docs/snapshot-testing.html). | ||
|
||
# Reasons to use this package | ||
|
||
> Most of this content is taken from the [Jest snapshot blogpost](https://facebook.github.io/jest/blog/2016/07/27/jest-14.html). | ||
We want to make it as frictionless as possible to write good tests that are useful. | ||
We observed that when engineers are provided with ready-to-use tools, they end up writing more tests, which in turn results in stable and healthy code bases. | ||
|
||
However engineers frequently spend more time writing a test than the component itself. As a result many people stopped writing tests altogether which eventually led to instabilities. | ||
|
||
A typical snapshot test case for a mobile app renders a UI component, takes a screenshot, then compares it to a reference image stored alongside the test. The test will fail if the two images do not match: either the change is unexpected, or the screenshot needs to be updated to the new version of the UI component. | ||
|
||
|
||
## Snapshot Testing with SnapshotTest | ||
|
||
A similar approach can be taken when it comes to testing your APIs. | ||
Instead of rendering the graphical UI, which would require building the entire app, you can use a test renderer to quickly generate a serializable value for your API response. | ||
|
||
|
||
## License | ||
|
||
[MIT License](https://github.com/syrusakbary/snapshottest/blob/master/LICENSE) | ||
|
||
[![coveralls][coveralls-image]][coveralls-url] | ||
|
||
[coveralls-image]: https://coveralls.io/repos/syrusakbary/snapshottest/badge.svg?branch=master&service=github | ||
[coveralls-url]: https://coveralls.io/github/syrusakbary/snapshottest?branch=master |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from .snapshot import Snapshot | ||
from .generic_repr import GenericRepr | ||
from .module import assert_match_snapshot | ||
from .unittest import TestCase | ||
|
||
|
||
__all__ = ["Snapshot", "GenericRepr", "assert_match_snapshot", "TestCase"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from termcolor import colored | ||
from fastdiff import compare | ||
|
||
from .sorted_dict import SortedDict | ||
from .formatter import Formatter | ||
|
||
|
||
def format_line(line): | ||
line = line.rstrip("\n") | ||
if line.startswith("-"): | ||
return colored(line, "green", attrs=["bold"]) | ||
elif line.startswith("+"): | ||
return colored(line, "red", attrs=["bold"]) | ||
elif line.startswith("?"): | ||
return colored("") + colored(line, "yellow", attrs=["bold"]) | ||
|
||
return colored("") + colored(line, "white", attrs=["dark"]) | ||
|
||
|
||
class PrettyDiff(object): | ||
def __init__(self, obj, snapshottest): | ||
self.pretty = Formatter() | ||
self.snapshottest = snapshottest | ||
if isinstance(obj, dict): | ||
obj = SortedDict(obj) | ||
self.obj = self.pretty(obj) | ||
|
||
def __eq__(self, other): | ||
return isinstance(other, PrettyDiff) and self.obj == other.obj | ||
|
||
def __repr__(self): | ||
return repr(self.obj) | ||
|
||
def get_diff(self, other): | ||
text1 = "Received \n\n" + self.pretty(self.obj) | ||
text2 = "Snapshot \n\n" + self.pretty(other) | ||
|
||
lines = list(compare(text2, text1)) | ||
return [format_line(line) for line in lines] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from django.test import TestCase as dTestCase | ||
from django.test import SimpleTestCase as dSimpleTestCase | ||
from django.test.runner import DiscoverRunner | ||
|
||
from snapshottest.reporting import reporting_lines | ||
from .unittest import TestCase as uTestCase | ||
from .module import SnapshotModule | ||
|
||
|
||
class TestRunnerMixin(object): | ||
separator1 = "=" * 70 | ||
separator2 = "-" * 70 | ||
|
||
def __init__(self, snapshot_update=False, **kwargs): | ||
super(TestRunnerMixin, self).__init__(**kwargs) | ||
uTestCase.snapshot_should_update = snapshot_update | ||
|
||
@classmethod | ||
def add_arguments(cls, parser): | ||
super(TestRunnerMixin, cls).add_arguments(parser) | ||
parser.add_argument( | ||
"--snapshot-update", | ||
default=False, | ||
action="store_true", | ||
dest="snapshot_update", | ||
help="Update the snapshots automatically.", | ||
) | ||
|
||
def run_tests(self, test_labels, extra_tests=None, **kwargs): | ||
result = super(TestRunnerMixin, self).run_tests( | ||
test_labels=test_labels, extra_tests=extra_tests, **kwargs | ||
) | ||
self.print_report() | ||
if TestCase.snapshot_should_update: | ||
for module in SnapshotModule.get_modules(): | ||
module.delete_unvisited() | ||
module.save() | ||
|
||
return result | ||
|
||
def print_report(self): | ||
lines = list(reporting_lines("python manage.py test")) | ||
if lines: | ||
print("\n" + self.separator1) | ||
print("SnapshotTest summary") | ||
print(self.separator2) | ||
for line in lines: | ||
print(line) | ||
print(self.separator1) | ||
|
||
|
||
class TestRunner(TestRunnerMixin, DiscoverRunner): | ||
pass | ||
|
||
|
||
class TestCase(uTestCase, dTestCase): | ||
pass | ||
|
||
|
||
class SimpleTestCase(uTestCase, dSimpleTestCase): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
class SnapshotError(Exception): | ||
pass | ||
|
||
|
||
class SnapshotNotFound(SnapshotError): | ||
def __init__(self, module, test_name): | ||
super(SnapshotNotFound, self).__init__( | ||
"Snapshot '{snapshot_id!s}' not found in {snapshot_file!s}".format( | ||
snapshot_id=test_name, snapshot_file=module.filepath | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import os | ||
import shutil | ||
import filecmp | ||
|
||
from .formatter import Formatter | ||
from .formatters import BaseFormatter | ||
|
||
|
||
class FileSnapshot(object): | ||
def __init__(self, path): | ||
""" | ||
Create a file snapshot pointing to the specified `path`. In a snapshot, `path` | ||
is considered to be relative to the test module's "snapshots" folder. (This is | ||
done to prevent ugly path manipulations inside the snapshot file.) | ||
""" | ||
self.path = path | ||
|
||
def __repr__(self): | ||
return "FileSnapshot({})".format(repr(self.path)) | ||
|
||
def __eq__(self, other): | ||
return self.path == other.path | ||
|
||
|
||
class FileSnapshotFormatter(BaseFormatter): | ||
def can_format(self, value): | ||
return isinstance(value, FileSnapshot) | ||
|
||
def store(self, test, value): | ||
""" | ||
Copy the file from the test location to the snapshot location. | ||
If the original test file has an extension, the snapshot file will | ||
use the same extension. | ||
""" | ||
|
||
file_snapshot_dir = self.get_file_snapshot_dir(test) | ||
if not os.path.exists(file_snapshot_dir): | ||
os.makedirs(file_snapshot_dir, 0o0700) | ||
extension = os.path.splitext(value.path)[1] | ||
snapshot_file = os.path.join(file_snapshot_dir, test.test_name) + extension | ||
shutil.copy(value.path, snapshot_file) | ||
relative_snapshot_filename = os.path.relpath( | ||
snapshot_file, test.module.snapshot_dir | ||
) | ||
return FileSnapshot(relative_snapshot_filename) | ||
|
||
def get_imports(self): | ||
return (("snapshottest.file", "FileSnapshot"),) | ||
|
||
def format(self, value, indent, formatter): | ||
return repr(value) | ||
|
||
def assert_value_matches_snapshot( | ||
self, test, test_value, snapshot_value, formatter | ||
): | ||
snapshot_path = os.path.join(test.module.snapshot_dir, snapshot_value.path) | ||
files_identical = filecmp.cmp(test_value.path, snapshot_path, shallow=False) | ||
assert files_identical, "Stored file differs from test file" | ||
|
||
@staticmethod | ||
def get_file_snapshot_dir(test): | ||
""" | ||
Get the directory for storing file snapshots for `test`. | ||
Snapshot files are stored under: | ||
snapshots/snap_<test_module_name>/ | ||
Right next to where the snapshot module is stored: | ||
snapshots/snap_<snapshot_module_name>.py | ||
""" | ||
return os.path.join(test.module.snapshot_dir, test.module.module) | ||
|
||
|
||
Formatter.register_formatter(FileSnapshotFormatter()) |
Oops, something went wrong.