Skip to content

Commit

Permalink
Vendor the snapshottest repo rather than pull from github.
Browse files Browse the repository at this point in the history
Works around new build issue.
  • Loading branch information
domdfcoding committed Jul 8, 2024
1 parent cf1fd3f commit 53857fa
Show file tree
Hide file tree
Showing 21 changed files with 1,249 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is managed by 'repo_helper'. Don't edit it directly.
---

exclude: ^$
exclude: ^snapshottest/

ci:
autoupdate_schedule: quarterly
Expand Down
1 change: 1 addition & 0 deletions repo_helper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use_whey: true
min_coverage: 95
tox_testenv_extras: all
standalone_contrib_guide: true
pre_commit_exclude: "^snapshottest/"

conda_channels:
- conda-forge
Expand Down
21 changes: 21 additions & 0 deletions snapshottest/LICENSE
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.
141 changes: 141 additions & 0 deletions snapshottest/README.md
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
7 changes: 7 additions & 0 deletions snapshottest/__init__.py
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"]
39 changes: 39 additions & 0 deletions snapshottest/diff.py
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]
61 changes: 61 additions & 0 deletions snapshottest/django.py
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
11 changes: 11 additions & 0 deletions snapshottest/error.py
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
)
)
73 changes: 73 additions & 0 deletions snapshottest/file.py
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())
Loading

0 comments on commit 53857fa

Please sign in to comment.