diff --git a/tests/utils/heptapod.py b/tests/utils/heptapod.py
index 4316399e132058a8228b3e31d156be16e386cb92_dGVzdHMvdXRpbHMvaGVwdGFwb2QucHk=..c0a71291b3acbe9113a1b115ee6da80f62a1d68e_dGVzdHMvdXRpbHMvaGVwdGFwb2QucHk= 100644
--- a/tests/utils/heptapod.py
+++ b/tests/utils/heptapod.py
@@ -1,6 +1,7 @@
 import contextlib
 from io import BytesIO
 import logging
+import os
 from pathlib import Path
 import requests
 import selenium.webdriver
@@ -4,6 +5,7 @@
 from pathlib import Path
 import requests
 import selenium.webdriver
+import shutil
 import subprocess
 import sys
 import tarfile
@@ -505,3 +507,43 @@
     def put_file_lines(self, path, lines):
         with open(path, 'w') as fobj:
             fobj.writelines(lines)
+
+
+class GdkHeptapod(SourceHeptapod):
+    """An Heptapod server running with the GDK.
+    """
+
+    fs_access = True
+
+    shell_access = True
+
+    reverse_call_host = 'localhost'
+
+    RAILS_SERVICES = ('rails-web', 'rails-background-jobs')
+
+    def __init__(self, gdk_root, **kw):
+        self.gdk_root = gdk_root
+        self.rails_root = os.path.join(gdk_root, 'gitlab')
+        super(GdkHeptapod, self).__init__(
+            repositories_root=os.path.join(gdk_root, 'repositories'),
+            **kw)
+
+    @property
+    def backups_dir(self):
+        return os.path.join(self.rails_root, 'tmp', 'backups')
+
+    def remove_all_backups(self):
+        if os.path.exists(self.backups_dir):
+            shutil.rmtree(self.backups_dir)
+        # as of GitLab 12.10, parent dir is always present
+        os.mkdir(self.backups_dir)
+
+    def rake(self, *args):
+        cmd = ['bundle', 'exec', 'rake']
+        cmd.extend(args)
+        subprocess.check_call(cmd, cwd=self.rails_root)
+
+    def ctl_services(self, command, services):
+        for service in services:
+            subprocess.check_call(('gdk', command, service),
+                                  cwd=self.rails_root)