diff --git a/apertis_tests_lib/__init__.py b/apertis_tests_lib/__init__.py
index 109589c31fa57b72a76e8e7ae323d28fe476d4e3..4cc81457bf7f11d439256c82fc16babe448f4dfc 100644
--- a/apertis_tests_lib/__init__.py
+++ b/apertis_tests_lib/__init__.py
@@ -11,6 +11,8 @@ import unittest
 import shutil
 import os
 
+from gi.repository import GLib
+
 MEDIADIR = '/usr/lib/apertis-tests/resources/media'
 
 # define this here to make lint happy with too long lines
@@ -18,6 +20,11 @@ LONG_JPEG_NAME = '320px-European_Common_Frog_Rana_temporaria.jpg'
 
 
 class ApertisTest(unittest.TestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.loop = GLib.MainLoop.new(None, False)
+        self.homedir = os.path.expanduser("~")
+
     def copy_medias(self, dst):
         """
         Copy media resources from apertis-tests to @dst with per media type
diff --git a/apertis_tests_lib/tracker.py b/apertis_tests_lib/tracker.py
index f0f04baa090342a01e03550d4639666edb9c4b2c..43a19600abd1d954000512668b21012537f8ab47 100644
--- a/apertis_tests_lib/tracker.py
+++ b/apertis_tests_lib/tracker.py
@@ -31,8 +31,6 @@ class TrackerIndexerMixin:
                               stdout=subprocess.DEVNULL,
                               shell=True)
 
-        self.loop = GLib.MainLoop.new(None, False)
-
         print("TrackerIndexerMixin: init done")
 
     def miner_progress_cb(self, manager, miner, status, progress,
diff --git a/apertis_tests_lib/tumbler.py b/apertis_tests_lib/tumbler.py
new file mode 100644
index 0000000000000000000000000000000000000000..74862efe2bbd43107a5b2c0af4d48486681a0ab4
--- /dev/null
+++ b/apertis_tests_lib/tumbler.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2015 Collabora Ltd.
+#
+# SPDX-License-Identifier: MPL-2.0
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import hashlib
+import os
+
+from gi.repository import GLib
+from gi.repository import Gio
+
+
+class TumblerMixin:
+    def __init__(self):
+        # Monitor thumbnail creation to know when it's done. The DBus API
+        # doesn't have a method to query the initial state but we can be
+        # reasonably sure it's not thumbnailing anything at this point.
+        self.tumbler_queue = []
+        self.tumbler = Gio.DBusProxy.new_for_bus_sync(
+            Gio.BusType.SESSION,
+            Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
+            None,
+            'org.freedesktop.thumbnails.Thumbnailer1',
+            '/org/freedesktop/thumbnails/Thumbnailer1',
+            'org.freedesktop.thumbnails.Thumbnailer1',
+            None)
+        self.tumbler.connect('g-signal', self.__g_signal_cb)
+
+        # Spy on unicast signals that weren't meant for us, because
+        # the Thumbnailer API uses those, and we want to use them to
+        # determine when it has finished. GDBus doesn't have
+        # high-level API for this sort of nonsense so we do it the hard way.
+        match_rule = ("type=signal,"
+                      "sender='org.freedesktop.thumbnails.Thumbnailer1',"
+                      "eavesdrop=true")
+        conn = self.tumbler.get_connection()
+        conn.call_sync('org.freedesktop.DBus',
+                       '/org/freedesktop/DBus',
+                       'org.freedesktop.DBus',
+                       'AddMatch',
+                       GLib.Variant('(s)', (match_rule,)),
+                       None, Gio.DBusCallFlags.NONE, -1, None)
+
+    def __g_signal_cb(self, proxy, sender_name, signal_name, parameters):
+        print('TumblerMixin: Received signal:', signal_name)
+
+        if signal_name == 'Started':
+            child = parameters.get_child_value(0)
+            self.tumbler_queue.append(child.get_uint32())
+        elif signal_name == 'Finished':
+            child = parameters.get_child_value(0)
+            self.tumbler_queue.remove(child.get_uint32())
+        elif signal_name == 'Error':
+            child = parameters.get_child_value(1)
+            uris = child.get_strv()
+            child = parameters.get_child_value(3)
+            msg = child.get_string()
+            raise Exception("Error creating thumbnail for %s: %s" %
+                            ', '.join(uris), msg)
+
+    def __get_supported_cb(self, source, result):
+        self.tumbler.call_finish(result)
+        self.loop.quit()
+
+    def tumbler_drain_queue(self):
+        # Drain the DBus queue to make sure we received all signals
+        self.tumbler.call('GetSupported',
+                          GLib.Variant.new_tuple(),
+                          Gio.DBusCallFlags.NONE,
+                          -1,
+                          None,
+                          self.__get_supported_cb)
+        self.loop.run()
+
+        context = self.loop.get_context()
+        while len(self.tumbler_queue) > 0:
+            context.iteration(True)
+
+    def tumbler_assert_thumbnailed(self, root, filename):
+        removable = False
+        monitor = Gio.VolumeMonitor.get()
+        mounts = monitor.get_mounts()
+        for m in mounts:
+            if m.get_root().get_path() == root:
+                removable = m.can_eject()
+                break
+
+        media_path = os.path.join(root, filename)
+
+        if removable:
+            dirname = os.path.dirname(media_path)
+            basename = os.path.basename(media_path)
+            digest = hashlib.md5(bytes(basename, encoding='UTF-8')).hexdigest()
+            thumbdir = os.path.join(dirname, '.sh_thumbnails')
+        else:
+            uri = 'file://' + media_path
+            digest = hashlib.md5(bytes(uri, encoding='UTF-8')).hexdigest()
+            thumbdir = os.path.join(self.homedir, '.cache', 'thumbnails')
+
+        path = os.path.join(thumbdir, 'normal', digest + '.png')
+        self.assertTrue(os.path.isfile(path))
diff --git a/tracker/automated/test-tracker.py b/tracker/automated/test-tracker.py
index d7eb21b5a0f213019cc78fc61d1ae41dd1e29c90..682c7083dabd24f72b006dffe17fd9b3413925c7 100755
--- a/tracker/automated/test-tracker.py
+++ b/tracker/automated/test-tracker.py
@@ -10,7 +10,6 @@
 
 import unittest
 import os
-import hashlib
 import sys
 
 from gi.repository import GLib
@@ -22,75 +21,26 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__),
                                 os.pardir, os.pardir))
 from apertis_tests_lib.tracker import TrackerIndexerMixin
 from apertis_tests_lib.grilo import GriloBrowserMixin
+from apertis_tests_lib.tumbler import TumblerMixin
 from apertis_tests_lib import ApertisTest
 from apertis_tests_lib import LONG_JPEG_NAME
 
 
-class TrackerTest(ApertisTest, TrackerIndexerMixin, GriloBrowserMixin):
+class TrackerTest(ApertisTest, TrackerIndexerMixin, GriloBrowserMixin,
+                  TumblerMixin):
     def __init__(self, *args, **kwargs):
         ApertisTest.__init__(self, *args, **kwargs)
         TrackerIndexerMixin.__init__(self)
         GriloBrowserMixin.__init__(self)
+        TumblerMixin.__init__(self)
 
     def setUp(self):
-        self.loop = GLib.MainLoop.new(None, False)
-        self.homedir = os.path.expanduser("~")
         self.copy_medias(self.homedir)
 
-        # Monitor thumbnail creation to know when it's done. The DBus API
-        # doesn't have a method to query the initial state but we can be
-        # reasonably sure it's not thumbnailing anything at this point.
-        self.tumbler_queue = []
-        self.tumbler = Gio.DBusProxy.new_for_bus_sync(
-            Gio.BusType.SESSION,
-            Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
-            None,
-            'org.freedesktop.thumbnails.Thumbnailer1',
-            '/org/freedesktop/thumbnails/Thumbnailer1',
-            'org.freedesktop.thumbnails.Thumbnailer1',
-            None)
-        self.tumbler.connect('g-signal', self.tumbler_signal_cb)
-
-        # Spy on unicast signals that weren't meant for us, because
-        # the Thumbnailer API uses those, and we want to use them to
-        # determine when it has finished. GDBus doesn't have
-        # high-level API for this sort of nonsense so we do it the hard way.
-        match_rule = ("type=signal,"
-                      "sender='org.freedesktop.thumbnails.Thumbnailer1',"
-                      "eavesdrop=true")
-        conn = self.tumbler.get_connection()
-        conn.call_sync('org.freedesktop.DBus',
-                       '/org/freedesktop/DBus',
-                       'org.freedesktop.DBus',
-                       'AddMatch',
-                       GLib.Variant('(s)', (match_rule,)),
-                       None, Gio.DBusCallFlags.NONE, -1, None)
-
     def tearDown(self):
         # Keep everything in place for further manual checks
         pass
 
-    def tumbler_signal_cb(self, proxy, sender_name, signal_name, parameters):
-        print('TrackerTest: Received tumbler signal:', signal_name)
-
-        if signal_name == 'Started':
-            child = parameters.get_child_value(0)
-            self.tumbler_queue.append(child.get_uint32())
-        elif signal_name == 'Finished':
-            child = parameters.get_child_value(0)
-            self.tumbler_queue.remove(child.get_uint32())
-        elif signal_name == 'Error':
-            child = parameters.get_child_value(1)
-            uris = child.get_strv()
-            child = parameters.get_child_value(3)
-            msg = child.get_string()
-            raise Exception("Error creating thumbnail for %s: %s" %
-                            ', '.join(uris), msg)
-
-    def tumbler_get_supported_cb(self, source, result):
-        self.tumbler.call_finish(result)
-        self.loop.quit()
-
     def tracker_config_tests(self):
         print("TrackerTest: config tests")
         settings = Gio.Settings.new("org.freedesktop.Tracker.Miner.Files")
@@ -133,31 +83,13 @@ class TrackerTest(ApertisTest, TrackerIndexerMixin, GriloBrowserMixin):
         self.wait(False)
         self.assert_not_indexed(filename)
 
-    def assert_has_thumbnail(self, filename, root=None):
-        # Note that this is the path for local storage only, not for removable
-        # devices.
-        if not root:
-            root = self.homedir
-        uri = 'file://%s/%s' % (root, filename)
-        uri_hash = hashlib.md5(bytes(uri, encoding='UTF-8')).hexdigest()
-        path = '%s/.cache/thumbnails/normal/%s.png' % (self.homedir, uri_hash)
-        self.assertTrue(os.path.isfile(path))
+    def assert_has_thumbnail(self, filename):
+        self.tumbler_assert_thumbnailed(self.homedir, filename)
 
     def thumbnail_tests(self):
         print("TrackerTest: thumbnail tests")
 
-        # Drain the DBus queue to make sure we received all signals
-        self.tumbler.call('GetSupported',
-                          GLib.Variant.new_tuple(),
-                          Gio.DBusCallFlags.NONE,
-                          -1,
-                          None,
-                          self.tumbler_get_supported_cb)
-        self.loop.run()
-
-        context = self.loop.get_context()
-        while len(self.tumbler_queue) > 0:
-            context.iteration(True)
+        self.tumbler_drain_queue()
 
         self.assert_has_thumbnail('Documents/lorem_presentation.odp')
         self.assert_has_thumbnail('Documents/lorem_spreadsheet.ods')
@@ -168,8 +100,8 @@ class TrackerTest(ApertisTest, TrackerIndexerMixin, GriloBrowserMixin):
 
         # Make sure it thumbnailed shared medias as well. This particular OGG
         # was known to have issues for thumbnailing.
-        self.assert_has_thumbnail('Music/Dee_Yan-Key_-_Lockung.ogg',
-                                  '/home/shared')
+        self.tumbler_assert_thumbnailed('/home/shared',
+                                        'Music/Dee_Yan-Key_-_Lockung.ogg')
 
     def grl_assert_common(self, medias, filename):
         self.assertTrue(filename in medias)
diff --git a/tracker/manual/test-removable-device.py b/tracker/manual/test-removable-device.py
index d320c15d795a2f7c415efd61571d890aac7599bc..0c267ed323f99e80cad5c771f055552cde7a0354 100755
--- a/tracker/manual/test-removable-device.py
+++ b/tracker/manual/test-removable-device.py
@@ -19,16 +19,18 @@ from gi.repository import Gio
 sys.path.insert(0, os.path.join(os.path.dirname(__file__),
                                 os.pardir, os.pardir))
 from apertis_tests_lib.tracker import TrackerIndexerMixin
+from apertis_tests_lib.tumbler import TumblerMixin
 from apertis_tests_lib import ApertisTest
+from apertis_tests_lib import LONG_JPEG_NAME
 
 
-class TestRemovableDevice(ApertisTest, TrackerIndexerMixin):
+class TestRemovableDevice(ApertisTest, TrackerIndexerMixin, TumblerMixin):
     def __init__(self, *args, **kwargs):
         ApertisTest.__init__(self, *args, **kwargs)
         TrackerIndexerMixin.__init__(self)
+        TumblerMixin.__init__(self)
 
     def setUp(self):
-        self.loop = GLib.MainLoop.new(None, False)
         self.monitor = Gio.VolumeMonitor.get()
         self.monitor.connect('mount-added', self.mount_added_cb)
 
@@ -40,6 +42,9 @@ class TestRemovableDevice(ApertisTest, TrackerIndexerMixin):
         self.path = mount.get_root().get_path()
         self.loop.quit()
 
+    def assert_has_thumbnail(self, filename):
+        self.tumbler_assert_thumbnailed(self.path, filename)
+
     def test_removable_device(self):
         print('Please insert storage ...')
         self.loop.run()
@@ -49,5 +54,13 @@ class TestRemovableDevice(ApertisTest, TrackerIndexerMixin):
         self.start()
         self.assert_all_indexed(self.path)
 
+        self.tumbler_drain_queue()
+        self.assert_has_thumbnail('Documents/lorem_presentation.odp')
+        self.assert_has_thumbnail('Documents/lorem_spreadsheet.ods')
+        self.assert_has_thumbnail('Documents/more_lorem_ipsum.odt')
+        self.assert_has_thumbnail('Pictures/' + LONG_JPEG_NAME)
+        self.assert_has_thumbnail('Pictures/collabora-logo-big.png')
+        self.assert_has_thumbnail('Videos/big_buck_bunny_smaller.ogv')
+
 if __name__ == "__main__":
     unittest.main()