Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

Your database is slow

Slide 3

Slide 3 text

Your database is slow • Writing to disk • Connections • Translating objects into SQL and back again

Slide 4

Slide 4 text

SPEED Hang on, why do I care? Faster unit tests means more tests, more runs and better uptake in your team. No one likes waiting!

Slide 5

Slide 5 text

Artist Album Track Record Label ART IST COLLABO RATORS

Slide 6

Slide 6 text

def track_details(self): return { 'number': self.number, 'name': self.name, 'album': self.album.name, 'artist': self.artist.name if self.artist else self.album.artist.name, 'collaborators': [ artist.name for artist in self.collaborators.all() ], 'label': self.album.label.name, }

Slide 7

Slide 7 text

def test_naive(self): label = RecordLabel.objects.create(...) artist = Artist.objects.create(...) album = Album.objects.create(...) track = Track.objects.create(...) other_artist = Artist.objects.create(...) track.collaborators.add(...) self.assertEqual(track.track_details(), ...) • 8 database queries • 5.2 seconds for 1000 runs on PostgreSQL • 3.9 seconds for 1000 runs on SQLite Naive PG Naive SQLite

Slide 8

Slide 8 text

def test_factories(self): track = TrackFactory.create(...) other_artist = ArtistFactory.create(...) track.collaborators.add(other_artist) self.assertEqual(track.track_details(), ...) • 8 database queries • 5.9 seconds for 1000 runs on PostgreSQL • 4.4 seconds for 1000 runs on SQLite Naive PG Naive SQLite Factory PG Factory SQLite

Slide 9

Slide 9 text

def test_factories_build_m2m_problem(self): track = TrackFactory.build() track.pk = 1 other_artist = ArtistFactory.create(...) track.collaborators.add(other_artist) self.assertEqual(track.track_details(), ...) • 4 database queries • Yuk! This is horrible and shouldn’t work at all...

Slide 10

Slide 10 text

def test_factories_build_prefetch(self): track = TrackFactory.build() track.pk = 1 other_artist = ArtistFactory.build(...) collaborators = track.collaborators.all() collaborators._result_cache = [other_artist] track._prefetched_objects_cache = { 'collaborators': collaborators, } self.assertEqual(track.track_details(), ...) • 0 database queries! • 1.0 seconds. About 4x faster! Hooray. • Doesn’t work if it’s not all() Naive PG Naive SQLite Factory PG Factory SQLite Prefetch

Slide 11

Slide 11 text

def test_factories_build_mock(self): other_artist = ArtistFactory.build(...) my_mock = mock.MagicMock() my_mock.all.return_value = [other_artist] with mock.patch('...Track.collaborators', my_mock): track = TrackFactory.build() track.pk = 1 self.assertEqual(track.track_details(), ...) • Still 0 database queries • 1.2 seconds • Kinda clunky, but more powerful Naive PG Naive SQLite Factory PG Factory SQLite Prefetch Mock

Slide 12

Slide 12 text

Summary • Avoiding the database is definitely faster! • Bigger difference than SQLite vs Postgres • Write your code in such a way to make avoiding the database easier

Slide 13

Slide 13 text

A better way? • “Mocking the database usually isn’t worth it” - Carl Meyer • Several implementations exist for redis • Manager / QuerySet is a defined API • (mostly...)

Slide 14

Slide 14 text

Introducing... https://github.com/mjtamlyn/django-test-db (catchier name suggestions are invited...)

Slide 15

Slide 15 text

Test-db • Not really a database at all • Patches QuerySet to provide the same API, but not hit your db • Ideal: Run your local tests fast, and then use production style db for CI • Prototype!

Slide 16

Slide 16 text

def test_test_db(self): label = RecordLabel.objects.create(...) artist = Artist.objects.create(...) album = Album.objects.create(...) track = Track.objects.create(...) other_artist = Artist.objects.create(...) track.collaborators.add(...) self.assertEqual(track.track_details(), ...) • 0 database queries • 0.7 seconds for 1000 runs - FAST! • Exact same code as naive test Naive PG Naive SQLite Factory PG Factory SQLite Prefetch Mock Memory DB

Slide 17

Slide 17 text

but... @mock.patch.object(RecordLabel.objects, 'get_queryset', lambda: QuerySet(RecordLabel)) @mock.patch.object(Artist.objects, 'get_queryset', lambda: QuerySet(Artist)) @mock.patch.object(Album.objects, 'get_queryset', lambda: QuerySet(Album)) @mock.patch.object(Track.objects, 'get_queryset', lambda: QuerySet(Track)) @mock.patch.object(Artist.collaborations.related_manager_cls, 'get_queryset', lambda instance: QuerySet(Artist)) @mock.patch.object(Track.collaborators.related_manager_cls, 'get_queryset', get_related_queryset) @mock.patch.object(Track.collaborators.related_manager_cls, '_add_items', add_items)

Slide 18

Slide 18 text

Issues • Some things don’t go through QuerySet • Auto-patching • More filter lookups • Extensibility

Slide 19

Slide 19 text

The future? • Write your code and tests “naively” • Use test-db locally for fast testing cycle • Deploy to CI and run exact same code against real DB • Help?

Slide 20

Slide 20 text

Questions? https://github.com/mjtamlyn/django-test-db Marc Tamlyn DjangoCon EU 2013