Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Mock your database

Mock your database

Databases are slow. Well, if the goal is 1 millisecond per test they are anyway. We want to avoid interacting with the database as much as possible when testing, especially if the tests aren't anything to do with the queries.

This talk will look at various ways of avoiding those pesky database queries and making tests faster!

Marc Tamlyn

May 16, 2013
Tweet

More Decks by Marc Tamlyn

Other Decks in Programming

Transcript

  1. Your database is slow • Writing to disk • Connections

    • Translating objects into SQL and back again
  2. 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!
  3. 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, }
  4. 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
  5. 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
  6. 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...
  7. 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
  8. 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
  9. 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
  10. 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...)
  11. 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!
  12. 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
  13. 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)
  14. 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?