Skip to content

Commit aace506

Browse files
committed
Redesign the poc
1 parent 41ebdad commit aace506

26 files changed

+443
-500
lines changed

README.rst

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,81 +12,83 @@ I'm pretty lazy when it comes to writing tests for existing code, however, I'm
1212
even lazier when it comes to repetitive manual testing action.
1313

1414
This package aims at de-duplicating the data import tests from
15-
django-representatives and django-representatives-votes which is to be re-used
16-
in django-cities-light.
15+
django-representatives and django-representatives-votes which is re-used in
16+
django-cities-light.
1717

1818
Database state assertion
1919
========================
2020

2121
A nice way to test a data import script is to create a source data fixture with
2222
a subset of data, ie. with only 10 cities instead of 28K or only 3 european
2323
parliament representatives instead of 3600, feed the import function with that
24-
and then compare the database state with a django fixture. For example:
24+
and then compare the database state with a django fixture. This looks like what
25+
I was used to do:
2526

2627
- use such a command to create a small data extract
2728
`shuf -n3 cities15000.txt > cities_light/tests/cities_test_fixture.txt`,
2829
- use it against the import script on a clean database,
2930
- verify the database manually, and run
3031
`django-admin dumpdata --indent=4 cities_light > cities_light/tests/cities_test_expected.txt`
31-
- then, make a test case that calls the import script against the fixture and
32-
call test_light's function to assert that the database contains only the
33-
expected data.
32+
- then, make a test case that calls the import script against the fixture,
33+
- write and maintain some funny (fuzzy ?) repetitive test code to ensure that
34+
the database is in the expected state.
3435

3536
When a bug is fixed, just add the case to the fixture and repeat the process to
3637
create new expected data dumps, use coverage to ensure no case is missed.
3738

38-
Predictible serialization
39-
=========================
39+
With django-dbdiff, I just need to maintain to initial data extract, and test
40+
it with ``Fixture.assertNoDiff('appname/path/to/fixture',
41+
models=[YourModelToTest])`` in a ``django.test.TransactionTestCase`` which has
42+
``reset_sequences=True``:
4043

41-
It is important to use serializers which dump data in a predictible way because
42-
this app relies on diff between an expected - user-generated and versioned -
43-
fixture and dumped database data.
44-
45-
Django's default model-to-dict logic - implemented in
46-
django.core.serializers.python.Serializer.get_dump_object() - returns a dict,
47-
this app registers a slightly modified version of the default json serializer
48-
which returns OrderedDicts instead.
49-
50-
In addition, dbdiff serialization forces Decimal normalization, because
51-
trailing zeros could happen in inconsistent ways.
52-
53-
Cross-database fixture compatibility
54-
====================================
55-
56-
MySQL doesn't have microseconds in datetimes, so dbdiff's serializer removes
57-
microseconds from datetimes so that fixtures are cross-database compatible
58-
which make them usable for cross-database testing.
44+
- if the fixture in question doesn't exist, it'll be automatically created on
45+
with dumpdata for the concerned models on the first run, raising
46+
"FixtureCreated" exception to fail the test and inform of the path of the
47+
created fixture, so that it doesn't mislead the user in thinking the test
48+
passed with an existing fixture,
49+
- if the fixture exists, it'll run dumpdata on the models concerned and GNU
50+
diff it against the fixture, if there's any output it'll be raised in the
51+
"DiffFound" exception, failing the test and printing the diff.
5952

6053
Usage
6154
=====
6255

63-
MySQL, SQLite and PostgreSQL, Python 2.7 and 3.4 are supported along with
64-
Django 1.7 to 1.10 - it's always better to support django's master so that we
65-
can upgrade easily when it is released.
56+
Example::
6657

67-
Install ``django-dbdiff`` with pip and add ``dbdiff`` to ``INSTALLED_APPS``.
58+
from django import TransactionTestCase
59+
from dbdiff.fixture import Fixture
6860

69-
When ``dbdiff`` is installed, ``dumpdata`` will use its serializers which have
70-
predictible output and cross-database support, so fixtures dumped without
71-
``dbdiff`` installed will have to be regenerated after ``dbdiff`` is installed.
7261

73-
Example::
62+
class YourImportTest(test.TransactionTestCase):
63+
reset_sequences = True
64+
65+
def test_your_import(self):
66+
your_import()
7467

75-
from dbdiff import dbdiff
68+
Fixture('yourapp/tests/yourtest.json',
69+
models=[YourModel]).assertNoDiff()
7670

77-
your_import_function()
78-
assert not dbdiff.diff('your_app/tests/some_fixture.json')
71+
The first time, it will raise a ``FixtureCreated`` exception, and the test will
72+
fail. This is to inform the user that the test didn't really run. On the next
73+
run though, it will pass.
7974

8075
If any difference is found between the database and the test fixture, then
8176
``diff()`` will return the diff as outputed by GNU diff.
8277

83-
A context manager that will raise an exception if a diff is found is also
84-
provided, it's able to delete models and reset the PK sequences for them::
78+
See tests and docstrings for crunchy details.
8579

86-
with dbdiff.exact('your_app/fixture.json'):
87-
# do stuff
80+
Requirements
81+
============
8882

89-
More public API tests can be found in dbidff/tests/test_dbdiff.py.
83+
MySQL, SQLite and PostgreSQL, Python 2.7 and 3.4 are supported along with
84+
Django 1.7 to 1.10 - it's always better to support django's master so that we
85+
can **upgrade easily when it is released**, which is one of the selling points
86+
for having 100% coverage.
87+
88+
Install
89+
=======
90+
91+
Install ``django-dbdiff`` with pip and add ``dbdiff`` to ``INSTALLED_APPS``.
9092

9193
Django model observer
9294
=====================

dbdiff/apps.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from django.apps import AppConfig
66
from django.core.serializers import register_serializer
77

8+
from .utils import patch_transaction_test_case
9+
810

911
class DefaultConfig(AppConfig):
1012
"""
11-
Default AppConfig for dbdiff.
13+
Register patched serializers and patch TransactionTestCase for sqlite.
1214
1315
.. py:attribute:: debug
1416
@@ -18,12 +20,25 @@ class DefaultConfig(AppConfig):
1820

1921
name = 'dbdiff'
2022
debug = False
23+
default_indent = 4
2124

2225
def ready(self):
2326
"""
2427
Register dbdiff.serializers.json and set debug.
2528
2629
Enables debug if a DBDIFF_DEBUG environment variable is found.
30+
31+
It is important to use serializers which dump data in a predictible way
32+
because this app relies on diff between an expected - user-generated
33+
and versioned - fixture and dumped database data. This method also
34+
overrides the default json serializer with dbdiff's.
35+
36+
When dbdiff is installed, ``dumpdata`` will use its serializers which
37+
have predictible output and cross-database support, so fixtures dumped
38+
without dbdiff installed will have to be regenerated after dbdiff is
39+
installed to be usable with dbdiff.
40+
2741
"""
2842
self.debug = os.environ.get('DBDIFF_DEBUG', False)
2943
register_serializer('json', 'dbdiff.serializers.json')
44+
patch_transaction_test_case()

dbdiff/dbdiff.py

Lines changed: 0 additions & 119 deletions
This file was deleted.

dbdiff/exceptions.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@ class DbDiffException(Exception):
55
"""Base exception for this app."""
66

77

8-
class EmptyFixtures(DbDiffException):
9-
"""Raised when a fixtures file is empty."""
10-
11-
def __init__(self, path):
12-
"""Exception for when path does not contain any fixture data."""
13-
super(EmptyFixtures, self).__init__('%s is empty' % path)
14-
15-
168
class DiffFound(DbDiffException):
179
"""Raised when a diff is found by the context manager."""
1810

19-
def __init__(self, out):
11+
def __init__(self, cmd, out):
2012
"""Exception for when a diff command had output."""
21-
super(DiffFound, self).__init__(out)
13+
super(DiffFound, self).__init__('%s\n%s' % (cmd, out.decode('utf8')))
14+
15+
16+
class FixtureCreated(DbDiffException):
17+
"""
18+
Raised when a fixture was created.
19+
20+
This purposely fails a test, to avoid misleading the user into thinking
21+
that the test was properly executed against a versioned fixture. Imagine
22+
one pushes a test without the fixture, it will break because of this
23+
exception in CI.
24+
25+
However, this should only happen once per fixture - unless the user in
26+
question forgets to commit the generated fixture !
27+
"""

0 commit comments

Comments
 (0)